In the last lesson, we created the user interface for minting NFTs and programmatically creating these 1
new containers which contain all unique non-fungible tokens. 2
And when we minted it, we got our loader to work, 3
we got our frontend to be formatted. And once the canisters are created in the backend, then we can pull the 4
data in and show the user what they had just created. 5
Now if we keep minting these tokens, we want a way to be able to view all of them, 6
right? 7
And we want to do that inside this tab, which is called My NFTs. 8
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. 9
Now, at this point in time, the NFTs that you own will be the same as the ones you mint, because that's 10
the only way that where you get hold of NFTs at the moment. 11
How do we get this to actually work? 12
Well, firstly, we have to get this navigation to work. 13
And to do that, we're going to use something called React Router to route us through to these different 14
routes and display a different page depending on where we're going. 15
So we're going to now comment out this minter and take our homepage back online, 16
so this last one. And we don't need this NFTID hardcoded here anymore. 17
So I'm going to comment that out as well. 18
And now if we go back, you should see our home page like this 19
and these links are still yet inactive. 20
So let's go ahead and fix that. 21
Now we're rendering the header, and in the header, we want to add some code to be able to take the user 22
to a different route depending on which of these buttons they pressed on. 23
When I created the package.json, I added in a dependency which is called React Router Dom. 24
And in addition to the React Hook Form, these are all React utilities that are not from the main React 25
module, but they provide a lot of really useful functionality that you will find frequently needing 26
when you're creating websites. 27
Coming in to our header.jsx, 28
we're going to import something called a browser router from the React Router Dom. 29
So let's go ahead and import that. 30
And the location will be react-router-dom. And if that is all spelled correctly, then we should be able 31
to use it inside here. 32
So just above our very first div, we're going to add a BrowserRouter and we're going to wrap it around 33
the entire div. Coming down to the very bottom div 34
we're going to wrap it all inside this BrowserRouter. 35
And what this is going to do is it's going to allow us to define what are called links, and the links 36
or rather Link as you'll import 37
it, also comes from the React Router Dom, and we can put those around some pieces of text like what we've 38
got here and it'll allow us to turn this text into an actual link. 39
And each of these links have an attribute called to, where you can define the actual route that you want 40
to render. 41
So we're going to have a route which is discover, and we're going to have a route called Minter which 42
wraps around this piece of text here. 43
And we're also going to have a route down here which is going to be called collection because it's my 44
collection of NFTs, right? 45
Just check and make sure that you've got a different route set for each of these three buttons and that 46
your link wraps the piece of text and not the button. 47
I've added a little bit of styling to the text just so that it shows up nicely purple and looking clickable. 48
Now that we've got our three links in here, the final piece to this navigation is we need to put in 49
what's called a switch. 50
And basically the switch is going to look at which of these links were triggered and to render the appropriate 51
React component depending on which path. 52
And we of course have to import it also from the React Router 53
Dom. So make sure that you spelt it correctly. 54
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 55
this Switch component, we're going to add some routes. 56
Route is yet another class that comes from the React Router Dom, and once we import it, we can create 57
a route component. 58
We need a total of four of them because we have three of these links here and we want one for the home 59
page as well. 60
So when we click here, it shows us the image and we click here 61
it shows us the various different pages and routes. 62
Now the route takes an attribute called path, and the path allows us to match to one of these links. 63
So basically it's going to look at what the path is. 64
So for example, if I had f/collection, then this would be the path. 65
And once it can match the path, then it will render whatever it is we tell it to 66
inside here. The first thing we want to match is the root route. 67
And in this case, what we want to do is we want to render this image. 68
So I'm going to cut this out from here and paste it in between this route component. 69
And in addition, I'll need to import it from here, 70
so I'm going to cut that line as well 71
and paste it up here. 72
So now the root path is going to render this image and I'm going to add the path for all the other routes 73
as well. 74
We've got the /discover and remember that these routes have to match to the to attribute 75
of your links. 76
So in the case of preventing typos, I would recommend just copying and pasting it into there. 77
Now I'm going to copy and paste this a few times and I'm going to add minter path and also the collection 78
path as well. 79
So we've now got four routes and with different paths. For the Discover route, 80
I'm just going to add an H1where it says Discover. We're going to come back to this a little 81
bit later on because this is going to be the page where we see all the NFTs that are listed for sale. 82
And until we manage to get our NFTs listed for sale, we can't actually work on this route yet. 83
But the next one though, the minter route, we can definitely work on. Again in between the open and closing 84
route component tags, we're going to add the component that's going to be rendered. 85
And in this case, it's going to be the minter component. 86
And if you select it, then hopefully it will auto import. 87
And finally, our very last route, which is the collection. 88
We're going to render the gallery component and again, make sure that got imported. 89
And if you take a look at the gallery component, it's really, really simple. 90
It's basically a bit of grid formatting and it's going to render a bunch of items. 91
At the moment, 92
we've just got one item in there and it's not even going to display properly because we haven't given it 93
an ID. 94
But we're going to solve that very quickly in this current lesson, when we create the gallery of all 95
the NFTs that our current logged in user owns. 96
Coming back over here, we've got pretty much all of these routes set out. 97
And there's just one last thing I want to add. 98
When you use this route component and you've got these paths, the / will match pretty much 99
every path. 100
So whenever it sees anything with a forward slash, it'll go here first and it will get really confused. 101
So one thing we can do is, as you know, with switch statements, they're going to check from top to 102
bottom. 103
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 104
match any of our previous routes, not just because it sees the / and it already immediately 105
thinks it should be the first one. 106
Alternatively, and I think this is a little bit better, we can add the keyword exact in front of the path 107
and this tells the router that unless you see this exact string, then don't match this route. 108
Now when do we want this to show up? Well, we want it 109
when the user first loads up the homepage. 110
But also we want the user to be able to click on this opend and to be able to render it. 111
So let's go ahead and add another link and it's going to link to the root route. 112
And I'm going to wrap this link around this H5 right here. Coming back to our frontend website, 113
you can see that when we go to our home page, our root route, even though it's no longer in here, 114
that main image, it's still being rendered from our router. 115
Now when we go to discover, we see an H1 for Discover. 116
When we go to Minter, we see our usual minting page. 117
And finally, when we go to My NFT, we've got blank 118
and that's because we've got some errors in our gallery. Because as I mentioned earlier on, the item 119
doesn't render without an ID. We can fix that quite easily by simply passing in the id prop and setting 120
it equal to our previous canisterID that we used. 121
Like so. 122
And now when you click on the My NFTs page, you can see it no longer is an empty screen. 123
This top text says Discover and it's because it's hardcoded in here. 124
Let's go ahead and make that something that we can pass in as some props. 125
So we'll add the props there and we'll call this props.title. 126
Now we have to go to the previous section in order to pass in that title. Right here where we've got 127
our gallery, 128
let's go ahead and say title= and we can name that page. 129
I'm going to call My NFTs, like 130
so. Now you can see that update dynamically. 131
And at the moment, this NFT is actually hardcoded to display here because remember inside gallery we 132
provided it's ID 133
hard-coded like this and it's only showing one item. 134
We want a whole bunch of items, as many items as the owner owns, to go in to the gallery. 135
To do that, we need to update our code a little bit in our main.mo, because at the moment 136
when we mint a new NFT, all we do is return the canister ID. 137
And in fact, I've got a typo right here and hopefully you don't. 138
But it should say newNFTPrincipal, although in this case it matched so it didn't actually create 139
an error. 140
In addition to minting the NFT, returning the principal ID, we also want to set up some form of data 141
store inside our opend canister that keeps track of which NFTs were minted and who the owners are. 142
And to do that, we're going to use our trusty HashMap. 143
So let's go ahead and import our HashMap. 144
And we're going to create a map of NFTs. 145
This is going to be a variable because we're going to be modifying it. 146
And I'm going to call it a map of NFTs. 147
And I'm going to set it equal to HashMap.HashMap. 148
And then I can add the data types that will be stored. 149
And it's going to be principals as keys. 150
And these are going to be the principals of each new canister that gets created for the NFT. 151
And then it's going to be linked to an NFT canister stored here. 152
So we have to import the NFTActorClass, 153
and this is going to come from the ../nft 154
/nft. 155
And we should now be able to tap into the NFTActorClass inside here. 156
And that is going to be the NFTActorClass 157
.NFT data type. 158
And now finally, we can go ahead and initialize our HashMap in the same way that we've done before. 159
So we're going to be using principal.= to check the equality of any principal IDs that's passed 160
in to be checked against the key in our HashMap. 161
And we're going to be using principal.hash to hash those keys. 162
Once we've created our map of NFTs, we want to add to it every single time we create a new NFT. 163
So after this line, let newNFTPrincipal where we get hold of the id of the canister 164
that our new NFT is hosted on, 165
then we can get hold of our map of NFTs and we can use the put method to put a new item into that Hash 166
Map. 167
The key is going to be the newNFTPrincipal, and the value is going to be the new NFT that was minted 168
right here. In addition to having a map of NFTs, 169
it will be really handy if we had a map of owners. 170
Because this way we can have a map of the principal IDs of the owners of NFTs who have minted on our 171
website, and we can match them to the NFTs that they own. 172
And we can bring it up in the gallery when they click on my NFTs. 173
So this again, is going to be created using HashMap.HashMap. 174
The key is going to be the principal IDs of the owners. 175
Now, here comes the tricky part, because it's not going to be the same as our map of NFTs, where 176
each NFT principal is mapped to one single NFT. 177
But because each owner can own several NFTs, we actually want to create a list of NFTs here. 178
So we need to import the list type. 179
And we're going to specify the data type as list.list, and then the list is going to consist 180
of principals. 181
And then finally, after the weird-looking two double angle brackets, we can initialize this HashMap 182
in the same way we did before because the keys are also made of principals. 183
So now that we've got our map of owners, we again can't simply just use a put here. 184
Instead, we actually have to go through a few steps to get hold of the existing list that's stored 185
for a particular user and then update that list and then push it back into the HashMap. 186
So I'm going to split out a separate function. Underneath this first mint function, 187
I'm going to close it off with some semicolons, and I'm going to create a new private function, which 188
is called addToOwnershipMap. 189
And it's a really long-winded way of describing what it is that this function is going to do. 190
It's a private function because we don't want any other class or any other code from outside our main 191
.mo to be able to access it. 192
And the addToOwnershipMap is pretty simple. 193
The whole purpose is just to figure out how to add the newly created NFT to this map of owners. 194
So it's going to take some inputs. 195
First is going to be the owner, which is the principal of the owner of the NFT. 196
And then I'm also going to need the nftID, which is going to be yet another principal. 197
And this is the newly minted NFT that I want to add to our ownership map. 198
As I mentioned, the first thing we have to do is to actually get hold of the NFTs that the user currently 199
owns. 200
So I'm going to call this ownedNFTs, 201
and whenever you try to get hold of a value from a HashMap, because the principal that you supply 202
could actually be a brand new principal, it could be a new owner who's just minted their very first NFT 203
in which case they don't actually exist in our map of owners, 204
so when you provide the key of the owner's principal, it might actually not exist in HashMap, in 205
which case there will also be no value. 206
So this is why if we call mapOfOwners.get and provide the owner principal, what actually gets returned 207
here, 208
the data type of this, is a Motoko option. 209
So that's the one with the question mark on it, 210
and it means that it could be nil and it might not contain any data. 211
So the way to work with this is to use a switch statement in order to reduce that uncertainty. 212
You've seen this before. 213
We first have to provide the data type that we're expecting, 214
so it's a list of principles. 215
And immediately we get this error because on the left-hand side, we're saying that it has to be a list 216
of principals, 217
but on the right-hand side, what we're actually getting is a optional list. 218
So it might be nil. 219
Now, what we have to do is add in that switch statement, 220
wrap this expression inside a set of parentheses, and then we open up a set of curly braces to define 221
what should we say the owneNFTs should be equal to in the case where this returns null? 222
And what should it be in the case where it actually does return a result? 223
In the first case, when it's null, we're going to return an empty list. 224
The way to write an empty list in Motoko is a little bit cumbersome. 225
I'm kind of used to writing empty arrays like this in most other languages, but in Motoko it's going 226
to be List.nil. 227
And then we provide the data type of the list, which is principal, and then we add a empty set of 228
parentheses to initialize this empty list. 229
In the second case where we've actually got a result, well then we're going to unwrap this optional 230
result and set the ownNFTs to the unwrapped version of that result. 231
So this is the safest way of dealing with options and the easiest ways at the moment anyways, when working 232
with Motoko. 233
Now that we finally got a solid version of the list, then what we can do is we can push our newly minted 234
NFT onto this list. So we can set the ownedNFTs to use the list.push in order to push the 235
nftID that's passed in to the ownedNFTs list. 236
And if I add my semicolon here, then that error should go away. 237
So what we've done here is we've safely gotten hold of the list of canister IDs that are owned by this 238
owner, and we've taken the previous version of that list, added the new nftID and then set that list 239
to equal the new updated version of the list. 240
So a little bit roundabout, but it is necessary in order to make it work. 241
Now finally, we can do our put into our map of owners where the key is going to be the owner, which 242
is the owner's principal id, and the value is going to be this newly updated list of nftIDs that that 243
owner owns, 244
so from here. And back up here, we actually have to call this method addToOwnershipMap in order to 245
trigger it when a new minting happens. 246
And we're going to pass in the owner as the first input and we're going to pass in the newNFTPrincipal 247
as the second input. 248
And once we add our semi-colon, then all our errors should go away 249
and we are now able to keep track of all the NFTs that are minted on our website and all the owners 250
and which NFTs they own. 251
And that's going to be really useful later on when we set up our buying and selling on the website as 252
well. 253
The final thing we need to do is to be able to bring this from the back end over to the frontend. In 254
our main.mo, 255
I'm going to create another method and this method is basically going to fetch that list of IDs and 256
turn it into an array that can be used on the frontend. 257
So this is going to be a public query function, func, and it's going to be called getOwnedNFTs and we're 258
going to pass in the principal ID of a user and hopefully we'll be able to return all of the NFTs that 259
that particular user owns. 260
So we're going to return that asynchronously 261
and it's going to be an array of principals which the syntax will look like this, square brackets and 262
then principal inside. 263
Now let's add our semicolon to make this error go away. 264
And we've got some more errors because we haven't yet returned what we promised to. 265
But we're going to very soon. 266
Now, what we want to do is we want to return what is contained as the value of the key that we're matching 267
with. 268
We're going to be sending over the user's ID, and using this mapOfOwners, 269
we're going to match the principal, get hold of the list of the NFT canister IDs and then return that 270
as an array. 271
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 272
IDs that belongs to this user that's coming in through the input. 273
Pause the video and give this a go. 274
All right. 275
So this is going to be extremely similar to what we did before to the extent where you could almost 276
just copy and paste it. 277
But I think at this stage, when the syntax is really unfamiliar, it's a really good idea to just write 278
everything out and wrap your head around it a second time. 279
So I'm going to call this variable userNFTs and I'm going to set it's data type to list.list 280
[Principal]. 281
So a list containing principal data types. 282
And then I'm going to use the switch statement to call my mapOfOwner.get and here I'm going 283
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 284
the case where this list is null, then I'm going to return a empty list of principal data types and 285
initialize it with my parentheses. 286
And in the case where it's not null, then I'm going to unwrap the result and return the unwrapped version. 287
So now with our new userNFT, we can actually return it by using that method that we saw a little 288
while ago, 289
List.toArray which converts this userNFT list into an array. 290
And if I add the semicolon here then all my errors should go away and we should now be able to test 291
this on the frontend. 292
So I'm going to go to my header.jsx and here I want to first of all import the main.mo. 293
I'm going to import opend, all lowercase because as I mentioned before, that's the name of our canister 294
as defined in dfx.json and then the from is going to be the file path where I can find my declaration 295
folder/opend. 296
So now we can use this canister definition inside our header. 297
And what I'm going to do here is create a new async function which is called getNFTs and this is 298
going to allow us to tap into our await opend. 299
and then the name of our function down here that we just created is called getOwnedNFTs and we 300
have to pass in the principal of the user whose NFTs we are interested in. 301
As I mentioned before in our index.jsx in order to avoid dealing with the log in flow, because 302
this project is complicated enough as it is, for simplicity's sake, we're simply just using the anonymous 303
users principl, which I've defined as a constant in the index. 304
jsx. 305
And you should have a copy of these two lines of code in your index.jsx as well. 306
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 307
it'll get imported. 308
So now we want to set this equal to a new constant, which is the userNFTIds. 309
Now we want to be able to console.log this just to confirm that it actually works. 310
So let's put the userNFTIds into a console log, 311
and finally we have to make sure that we actually trigger this function and it's going to be triggered 312
the first time that this component is rendered. 313
So we're going to use the useEffect hook. And as usual, our useEffect hook is going to take a fat arrow 314
function, 315
and then the second parameter is going to be an empty array to make sure that this function only gets 316
triggered once. And the function is going to get triggered to call our getNFTs. 317
And now if I hit save and also make sure that my main.mo is saved as well, then I can go ahead 318
and redeploy my canister to make these changes available on the frontend. 319
So going back over to our read me and copying our deploy code right here. 320
So let's redeploy our canisters and then we can check out the changes on the frontend. 321
Now if I go ahead and mint something new from my list of cryptodunks and once this is minted, I'm going 322
to pull up my console. 323
You can see that when we're printing our console log userNFTIds, it's giving us this array. 324
And because I've been testing this a few times, there's currently three items that have been minted 325
that belong to this current user. 326
Now you might just have one and that's totally normal. 327
So now 328
if we go into our My NFT IDs, we want to be able to pass this userNFTIds to that gallery component 329
and to render all of those IDs each as an individual item. 330
And I'm going to use the useState hook to hold this gallery component. 331
So let's create a new consent called the userOwnedGallery, 332
and the setOwnedGallery. 333
And it's going to start off with absolutely nothing, 334
so our useState will be empty. 335
And then once we've gotten a hold of our useNFTIds, then we're going to set the owned gallery to 336
what we have down here. 337
So I'm going to cut this out and replace it with that userOwnedGallery in here. 338
And I'm going to paste what I copied over into here. 339
Our title is going to be My NFTs and the other prop I'm going to pass in is something called ids. 340
These will be the idss of the NFTs that need to be rendered as item components, and we're going to pass 341
in the userNFTIds here. 342
And once we've done that, then we can go into our gallery and we'll be able to catch it inside the 343
props. 344
So now, instead of rendering each item individually, I want to be able to add something called items, 345
and these items are going to be rendered programmatically. 346
Let's create a const called items and a setter could setItems, and we're going to use state. 347
And just make sure that it gets imported. 348
Now, next, we're going to create a new function, which I'm going to call fetchNFTs. 349
And this function is going to look through our props and make sure that the props.ids is not equal to 350
undefined, 351
so hopefully we've passed in some IDs. Then we're going to call setItems in order to create some item 352
components. 353
Now the way we're going to do this is through the map function. 354
You've seen this before when we did Keeper, but we're going to do props.ids 355
and we're going to call the map function. In this map function, 356
it expects a callback where each of the items inside this array will be mapped to a new constant. 357
So let's create another set of parentheses inside the parentheses. 358
And here we'll be able to get the NFTid, which is each of the items inside this array as it loops 359
through it, 360
and then we can use the fat arrow to define what it is we want rendered. 361
And that requires opening up another set of parentheses, not curly braces 362
so be careful here with the syntax, and we're going to render the Item component. 363
And if you remember, the Item component expects a prop called ID. 364
So we're going to set its id to the NFTId that we're looping through like so. 365
And then we can close off that component. 366
You might also remember this from the Keeper project when we did our React module. 367
But when we're mapping through an array and rendering components like this, 368
React likes us to have a key prop set. 369
It doesn't really care what exactly this key is, as long as it uniquely identifies each of the components, 370
so they're different. 371
In our case, we're going to use this NFTId that we've got and 372
we're going to call toText on it, which of course comes from the principal module, 373
so we have to import that up here. 374
And that will basically turn each of the NFTIds to a text value that we can keep track of and uniquely 375
identify each item component. 376
Now finally, we're ready to use our useEffect and go ahead and add our callback 377
and also our empty parentheses, and inside the callback we'll call our fetchNFTs method. 378
The very last thing we have to change is our item.jsx, 379
but just a little bit, because remember that what we're getting hold of in the props.ids are actual 380
principal ids to loop through each one of them and pass it over as the id. 381
These are not text ids, they're actually the principal data type, which is why for the key, we actually 382
had to convert it to text. 383
So over here we're expecting that to be text instead of actual principal ids. 384
All we have to do is delete those parts and simply leave it as props.id. 385
Now let's hit save and let's go back and you should be able to see the NFTs rendered. 386
Now they're looking a little bit weird on different lines because I think this final div actually has 387
its closing div right here and our items need to go inside that grid 388
in order for this to be rendered correctly. 389
You might have made this mistake or you might not have, but just check if you have it looking weird 390
like that. 391
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, 392
once that NFT is minted, I should be able to go into My NFTs, and if I hit refresh, then you'll see that 393
NFT show up. 394
Now it's a bit weird having to click refresh. 395
So what we can do is in our header, in our browser router here in the opening component tag, we can 396
add an attribute called force refresh. 397
And we can set it to true like this. 398
And what this is going to do is it's going to refresh our header, which means our useEffect gets called, 399
which means it's going to fetch the NFTs again and it's going to push it into that collection route. 400
So let's test this out again. 401
Let's try and mint something we haven't got on here. 402
Let's do this guy with the weird stripy cap. 403
And you can see now, when I head over to My NFTs, it forces a refresh and that guy shows up in 404
here exactly like how we would expect it to. 405
Hopefully this has worked out alright for you. 406
If not, as always, the completed project up to here will be downloadable. 407
But in order to continue with our project, the next step is setting up our NFTs for sale and listing 408
them into the Discover page. 409
So for all of that and more, 410
I'll see you in the next lessons.