In this chapter, we're going to go ahead and develop tenant pages. So in the last chapter, we actually implemented the multi-tenant functionality. So right now, we have the ability for each tenant to have their own product, right? And when they go to their dashboard, they can only see their own products besides the admin. Admin can see everyone's products, which is exactly what we wanted.
So now we're gonna go ahead and extend on that information by starting to load tenant, which basically means shop. Whenever you hear tenant, think of a shop, think of a store on our website, right? So we will load shop information into our product card. Let's start by properly having the shop's name here instead of hard-coded Antonio. As always, ensure that you are on your master branch, ensure that your graph looks correct.
You can always run git status to confirm. Now let's do a bun run dev here. And let's go inside of source. Let's go inside of app here. Actually I'm going to go inside of modules.
My apologies. Modules, products, UI, components. Let's go inside of the product list, because this is where we pass the author username. So now what I want to do is I want to ask, I pass product, tenant, and then in here, actually, it's always gonna have a tenant. In here, I want to pass the, Should it be the slug or the name?
I think we can pass the name, but we have some improper types here. So let's go ahead inside of getMany, and let's see how we can improve that. So right here, where I do const data await context database.find, I know that I use def one, which means that I populate the category and the image. And that now also includes tenant. So tenant will also be populated, which means that I can now add tenant, doc.tenant as tenant from payload types.
And We also know that the tenant will be required, right? So no need for us to add or null. Tenant is required for every single product that is created. So now you can safely pass product.tenant.name here. So if you want to, you can do one thing here.
You can do a console.log and JSON stringify data.tenant. Actually just the data.docs, right? And then when you do a refresh on your localhost, just make sure that you are running. So I think that I'm on my all page. In here, oh, I should have added null 2, so it's printed out in a prettier way.
So now in here you should see a tenant. There we go. But if you change the depth to zero, I think that then it looks like it doesn't fail, right? But it definitely doesn't do what it should, as you can see right here. So make sure the depth is at least 1.
And then you will see that the tenant is properly populated. In case you forget to put depth, I confirmed with the theme from payload, the default depth is actually two. So you don't have to worry, but I like to be explicit. I like to know why I'm adding this. Great, so now you can see that this says John and this says Antonio.
Great. Now, if you are aware of the tenant collection structure, you also know that we have an image, which means that we can also populate the author image URL, which is product.tenant.image. But it's actually .url, same way as this. And that is definitely going to be optional here. And let's also put question marks here just in case, right?
I don't want our front end to break. So I'm going to add a question mark dot URL here, and I'm going to go back inside of my procedures. And I'm just going to extend the tenant to also have an image, which is a type of media or null. So basically the same thing as this. And now it's properly being treated because we know that this will be populated.
As you can see right now, I don't have any images here. But what I'm going to do is I'm going to go inside of my Antonio tenant here, so AntonioDemo.com, demo. And I'm going to go inside of my dashboard, and I'm going to add an image for my tenant, right? So right now if I click on tenants, I can also only see the Antonio tenant. I can't see John's tenant.
So go inside of your Antonio tenant, and let's upload an image here. So I uploaded an image and I will just call this Antonio in the alt and I will click save. And then I will click save again. So I just want to see does this load the image or not? I'm curious because depth one might not be enough to load something that's inside of something we loaded.
Perhaps in this case, we might need depth two. So let me go here. And it looks like it's not loading. Let's see whether that is correct by taking a look here. Looks like it cannot read the properties of URL.
So What we have to do is we have to go inside of here and change the depth to two. So this will populate tenant and tenant.image now. Now I'm pretty confident it will work. And there we go. You can see how my image immediately pops up.
So that's how you have to be careful, right? So when it comes to our get many base procedure in the products router, we want to have depth two. So we populated the category, the image, the tenant, and also the tenant image. This is the depth two part. This is the first level and this is the second level.
That's what we wanted to have right here. Excellent. So now ensure that you don't have any errors here. You can see how I had a clear error here, right? It couldn't read that undefined.
So it would also be useful for us to... I'm not sure where do I use it like this? Because in here, It's written almost as if I don't add these question marks. But make sure you add these question marks, because you can see it can be tricky if you mess up the depth. But yeah, by default, this should work.
If I just add a comment here, you can see that everything will stay the same because depth 2 is the default. I'm not sure if it's documented, but depth 2 is the default. So you didn't have to worry, but make sure that depth is set to 2 here. Excellent. So now what I want to do is enable that when we click on Antonio, I want to be redirected to the actual tenant slug, right?
That's what I want to do. So let me go ahead and mark this as completed. And in order to do what I just said, we need to create the tenant shop page, right? But before we do that, let's first add the tenant slug filter to the products get many feature, because that will allow us to query by an individual tenant. So I'm going to go back inside of my get many procedures here.
And after all of these, I'm just going to add tenant slug, which will be a string and nullable and optional, like this. And now let's go ahead and use the tenant slug. So I'm going to do that just before I do the category here. If we have input tenant slug, we're just going to target the where for the tenant dot slug to be equals input tenant slug. That's it.
That's all the code we need to query by a tenant. Perfect. So now that we have the tenant slug, we can check this as done. And now let's create the tenant shop page. So as you know, we will have things like slug.domain.com.
But in order to do that, in future, rewrite to that. So this is how we are going to keep the shop internally. And then we're gonna use the middleware to rewrite to this. So in case you were worried, you know, but I want this subdomain, don't worry, we will have the subdomain, but we first have to create the internal route. So let's go inside of source app folder here.
And let's go and create inside of the app route group tenants like that. And then inside, create tenants without brackets and then slug like this. And then inside of here, let's go ahead and create a page.psx like this. And now in here, what we can do is we can create the interface page props, import the search params from nooks server. And we can just call these props actually.
So basically the same thing that we have in our two category routes, right? And we're also going to import the product list view, the load product filters from not from hooks use product filters. It's going to be from search params. Yes, that's the correct one. We are going to add hydrate clients and the DRPC from at the DRPC server.
And we're going to add the default limit constant, right? And let's change this to import type. Then let's do a default export of this page here. Let's mark it as an asynchronous page. Let's destructure the params and search params.
Let's add page props here. Page, my apologies, props. And then in here we can destructure the slug from await params. And then we can get the filters from await load product filters search params. Basically, the exact same thing that we are doing in a category page, right?
But instead of slug, it's category. You can see that this is the exact same thing. That's the power of these reusable things we've created. Now let's go ahead and let's copy these. And let's add that here.
So we are getting the query clients let me just import this from server I completely forgot about that and we don't have hydrate client my apologies we're going to use hydration boundary yes so this is query client prefetch infinite query and instead of category we will have tenant slug and match that to be slug like that And then in here you can just copy this thing like this and import hydration boundary and dehydrate from 10 stack react query. Let me move it here with the imports. And instead of passing the category, we're going to have to introduce a new prop tenant slug to be tenant slug. There we go. And now this is just slug in this case.
And now let's go inside of the product list view and let's add tenant slug, which is going to be an optional string here. And now let's go ahead and use the tenant slug in here. So now we have to, let's also import it, I mean, destructure it. Now let's go inside of the product list component and add it here. So we are doing some prop drilling, but it's okay, this is reusable and it's going to make our lives easier.
And now I want to pass the tenant slug here. There we go. So now we can ensure that this page prefetch infinite query 100% matches all the possible combinations in this case. So now we should be able to do the following. You should be able to manually go to localhost 3000 slash tenants and then slash Antonio.
So this should load the tenant page. And there we go. You can see that we successfully loaded Antonio's page and it only loads the Antonio's product. If I change my URL to go to John, it will only load John's product. So just like that, we have created these individual shop pages.
And now we're just going to create their layout in a bit of a better way. So let's go ahead and do the following. Inside of app folder, app route group tenants, inside of here, I want to go ahead and create a better layout. And I actually want to do this. I want to go in the slug and create another route group called home and then move this page inside because later we're gonna have some more route groups here.
If it asks you to update the imports you can press yes and then you're going to have this weird unsaved file. Just save it, close it and make sure you close the .next folder so you don't accidentally write something inside. And then just safely go back inside of your home route group in the tenants slug here. And inside of the route group create the layout file which we are now going to use. So in here I'm gonna go ahead and create the layout And I'm going to create a div with a class name, minimum height of screen, a background of F4, F4, F0, flex and flex column like this.
And then I'm just going to go ahead and create an interface layout props, which will accept the children, but also the params, which will hold our tenant slug right here. So I'm going to extract the layout props here, children and params. And for now, I can just render the children. So if you go ahead to localhost 3000 tenants Antonio, everything should stay the same. Nothing should change for now, except the background color.
The background color should be a little bit different. So now what I want to do here is I want to add a navbar component here. We are not yet going to be using these params. You can remove them if you want to. So now we're going to create a very simple navbar component here.
So we can do that by going inside of the modules and let's create tenants. And let's create UI folder and then components and then inside create the navbar.dsx. Let's export const navbar here and let's return instead of a div a nav element with a class name height20, border-bottom, font-medium, and background-color of white. And then in here, let's create a div with a class name, maximum-width, and This is actually a new Tailwind version 4 last name, dash dash breakpoint dash Excel. So usually you would do this with maximum width screen LG, something like this.
But there we go, you can see how it's warning me now, right, CSS conflict. So this is Tailwind version 3 and this is Tailwind version 4. I told you we're gonna learn something new about Tailwind in this tutorial, even if it took us 9 hours to get there. Now let's do mxauto here. Flex, justify between, items center, height full, px4 and on large px12.
We are basically creating a very simple navbar which is going to just render the word tenant for now. And let's give it a class name of TextExcel. Like this. Just a navbar. And then you can import this navbar from tenants.
So make sure you use the tenants navbar. And if you refresh here, you should see the tenant navbar right here. Now let's do the same thing but for the footer. So I'm going to go ahead and add a footer component here, which we are going to build in the tenants UI. So copy navbar, paste it, rename it to footer like this, and rename this to footer as well.
And the only difference here is that I'm going to add the app logo here. So I'm importing poppins here and CN and link. And then I'm just going to call this poppins. And the only weight we actually need is 700, like this. Let's change the nav element to footer element and let's change this class name to not have any height and instead have a border top, font medium and background white can stay.
And this entire class name can stay as well, so we need the same breakpoint. And then in here, we're going to do a text powered by... We can remove all class names. And Then we're going to do a link with an href to go back to the root page with a span fun-road with a class name CN, text to Excel, font semi-bold, and logo, my apologies, pop-ins class name. So now you can go inside of the layout and you can import the footer from module-stennands-ui-components-footer.
Refresh. And now you can see the footer right here. Let's go ahead and just quickly improve it so it looks just a little bit better. We don't need the justify between here. So it should just be powered by Funroad like this.
And let's see, let's add a gap to here. There we go. And if you click on Funroad, you get redirected back to the root page. So on each shop page, you will see your app's name, your platform's name. And now let's go inside of the layout, and let's wrap the children around a div here, and give it a class name of Lex1.
And then another div here. And in here, we're just going to go ahead and add a class name for the breakpoint, right? So we want to add this maximum width and MX auto. So basically, we want everything to be a little bit pushed when it comes to the tenant page. So there's a clear difference between this page, right?
And then when we go on the individual tenants page. And I don't like the footer. So let me just go here and let me add besides PX, let me add PY6. Wait for the refresh. There we go.
Now it looks better. Great. So now we can access individual tenants' page. I can change my URL to John and I can see the John's product now. But now, wouldn't it be cool if we could load the name of the tenant right here and also display their shop's name and display their shop's image?
So we can actually do that. In the layout, we can activate a prefetched based on the promise here. In order to do that, we first have to create the tenant router. So let's go inside of modules and let's copy a simple one. For example, the tags procedures.
Let's copy them and let's paste them inside of server here. Let's just paste the procedures inside of the tenants modules. And instead of get money, we will have get one. And the only thing we're going to look for is the slug, which will be a string. And the data here will do the following.
Tenants, awaitContextDatabase.findCollectionTenants. And we're going to do where slug equals input.slug. And then we're also going to make sure the limit is one and pagination is set to false. And let's do const individual tenant is tenant.docs, My apologies, tenants. We can call this tenantsData.
So tenantsData.docs and then the first one in the array and we can check if there is no tenant we can throw new TRPC error with a code not found and a message and not found You can import the TRPC error from at the TRPC server from the actual package. Otherwise, just return back the tenant. Now we have to include the tenant's router, or tenant router. Let me just see, products, procedures, how do I call it? Products.
Okay, so this will be tenant's router. Now let's go inside of the PRPC folder, inside of the underscore app, inside of the Routers folder, and we are just going to add tenantsRouter and import it. There we go. So now you should be able to go to your tenants layout right here, mark this as an asynchronous method, and now we're going to do the prefetch. We just need to destructure the slug from the params.
We can now add params here. And then just void and let me just go to a random page so I can see what I need. We need the query client, so I can just copy all of this. We need the query client from the RPC. Get query client from at the RPC server.
We need the RPC from the RPC server. We are not going to have any filters, category, or limit. We're just going to pass the slug to trpc tenants get one. So just pass in the slot. And it's going to be query options.
And just prefetch query. There we go. So once you've done this, you're going to go ahead and wrap the navbar inside of hydration boundary and you're going to pass let me just see the state to dehydrate the query client Import that from 10 stack query alongside the hydration boundary. And now your navbar will have prefetched the actual tenant. So what you can do now is you can prepare const ERPC from use ERPC from client, mark these as use client.
And now you can do const and get the data from use suspense query from 10 stack react query the RPC tenants get one query options. And in here, you should pass the slug. So we're going to create interface, rops, slug, string. And we are going to destructure the slug from here. And now you can safely pass the slug.
There we go. And now you can use data as if it was already loaded. So you don't have to do any if loads. You can already use it here. You can do data.name.
The data will 100% exist when this component loads. You just have to pass in this log here. So now when you refresh the Antonio, it will say Antonio. When you go to John, it will say John right here. And it prefetches that.
Now let's go ahead and create the proper skeleton here so we can load this even faster. So I'm going to copy this and export const navbar skeleton. I will just pass this And what I'm going to do here is I'm just going to keep this as an empty div because later there will be to do skeleton or checkout button. Right now it doesn't exist. And everything else can stay the same.
So you can just use the navbar skeleton here in the layout. So you can add a suspense here. Fallback, navbar, skeleton. Make sure you have imported both of them. So now for a split second, nothing should be displayed.
That's our skeleton. You can see how now when I refresh, it's in the cache. But if I switch to Antonio, this is the case where it actually can't find something. And it will start throwing errors. If we can actually handle this in a different way, I'm going to explore the error handling later.
There are several ways we can do that. But for now, just go to a proper slug name, Antonio here. Now, I also want to load the image if it exists, right? So let's go inside of the navbar here and let's load the user's image. So we're gonna do that here.
Let's also add a link, next link here. Let's give it an href. For now, it's going to go to slash tenants. It's going to go to data. Actually, you can just go to Slack, like this.
Give it a class name, flex-item-center-gap-2. Now in here, we're going to do if data-image? URL, Then we're going to go ahead and render an image from next image with a source of tenant image, oops, data image URL. Ignore the TypeScript errors for now. I'm going to explain what's happening in a second.
Let's give a width and height of 32 and a class name rounded full border shrink zero and the size of 32 pixels and an ALT can be slug. So why does it say that URL does not exist? We can clearly see that the code works. Well, again, it's because of our get1 here. So by default, it uses depth two, right?
This is the default. If you want to, you can set it to depth one, which will mean that tenant.image is a type of media because it's going to be populated. But we've already explained that payloads local API does not change TypeScript depending on the depth. So we're going to have to do this ourselves in this case. So what we can do here is return tenant as tenant from payload types and add media, my apologies, image to be a type of media from payload types or null because it's not required for the user to have an image, for the tenant to have an image.
So import those two. And now everything should still work just fine. You can see that it works with depth one, but if you change to depth zero then it no longer works so in this case dot image is on the first level of depth so depth one or depth two are going to work equally you can choose which one you want to use that one should work but of course confirm right make sure it works and you can see that now no more errors in this part at all. And if you click, yeah, right now it doesn't make sense. But this will be used later when we actually implement this, right?
This will usually lead to the products page. And I want to do one more thing. So let me see. Right now, when I go, I only use tenants slug. Whoops.
I only use tenants slug here. So I want to implement one method, which we are going to heavily use later on, and I already want to prepare for it. Let's go inside of the utils. So let me get inside of source lib utils. And in here, let's export function generate tenant URL.
And pass in the tenant slug, the string. My apologies, this is not an arrow function. And for now, what we're going to do is just return slash tenants and then slug. That's the only thing that we're going to do for now, right? But later, this is going to come in handy.
So let me go inside of my navbar where I load the tenant. And we're just going to pass in the generate tenant URL and pass in the slug. So nothing should change right now, but later this will help us because of that subdomain thing that I'm pretty sure all of you are eagerly waiting for, right? So nothing should change now, this should still work just fine. And now let's also enable that when user clicks on the name of the user in here it also redirects to the shop since the product card itself is already a link we cannot nest another link inside.
Instead, we're going to do this, right? We added to do redirect to user shop. So what we can do is we can do handle user click, like that. And then we're going to have to stop propagation, so this link doesn't activate. So const handle user click will get the event, which is a type of React.mouseEvent, HTML div element, event, preventDefault, event, stopPropagation.
And then we're going to have to add const router to be useRouter from next navigation. And router push generateTenantURL and pass in the author username, which we know is the actual, oh yeah, this is important, actually. This is very important. This would not always work now. So make sure that inside of the ProductList component, which is located inside of Modules, Products, UI Components, ProductList, where you render the product card, For the author username, make sure you use product tenant slug instead of name.
And here's an even better thing. Let's change this to tenant slug and let's change this to tenant image URL. Let's be specific about what we are using. Now let's go inside of the product card and let's modify this. So tenant slug and tenant image URL.
So we never confuse these two things with what they actually are. So now we can use the tenant slug here. Let's see where else. Here in the alt, and these two are tenant image URL. And in here, use tenant slug.
And that's it. I think everything else works just fine. And we can now remove the to do from here. So make sure that you have, I'm not sure if you need the react import because we are only using it for types. But now this should work.
If you now click on Antonio, you should get redirected to the Antonio's shop. If you click on John, you should get redirected to John's shop. And this should still work, by the way, right? Every single other filter should definitely work. If I put minimum price 99, it works, but 101 and no products found.
If I clear, they're back. So everything should still work. That's the power of all of these reusable components we have created. Amazing, amazing job. So what we can confidently start doing Next is building the actual product page.
We've created this shop. The only thing I might do in this shop is maybe change the grid a little bit because I've noticed since we do this maximum width restriction, the cards look very narrow. So I might change the grid to allow the cards to take more space than they usually do, because this looks completely fine in here. But in here, where we do a smaller screen, they look a little bit narrow. So maybe I'm going to make them a little bit wider.
We can actually do that in this chapter. We are already here. Let's start with the product list component. Inside of the product list component, add a new narrow view, optional Boolean component. Go ahead and destructure it from the props here.
Once you have the narrow view component, we're going to go and find the grid here. Go ahead and change this from the static CSS to now be CN, which is our util class names. You can import CN from at lib utils. Now, we can keep this as the default class and add if we have narrow view we're going to change it on large grid columns to on Excel grid columns three on to Excel grid columns three Ensure that all of these are properly written. And that's pretty much it.
Now what we have to do is find where we render the product list. There's only one place in the product list view, which means that in here, we need to also prop drill the narrow view optional Boolean, extract it from here, and then pass it here, like this. And then what we have to do is we have to go inside of the tenant page. So inside of tenants slug home page and simply pass narrow view to this. And already it should look better.
But now you can see that the skeleton does not match. So let's go inside of the product list view here and let's pass narrow view to the skeleton as well. Let's go inside and edit the product list skeleton. Narrow view, and we can just copy the div. So yes, we can modify the skeleton in the same way.
Why not? We want to make it look good. And now, even the skeleton matches exactly what we are going to see. You can see how now it just all looks much better. Let me just see if this skeleton...
Now this skeleton doesn't seem to be looking too good. So let me just see what this is about. For this one. Yeah, this is interesting. Why does it look so much bigger?
I'm not sure. It's because in the product list view component, we hard-coded this instead of passing it as a prop. Whoops. Narrow view. There we go.
So inside of the views, product list view, make sure that the props for the narrow view are props. And now the skeletons will match exactly what they need to be. And the user site will actually look good now. It looks unique because it cannot be as wide. And the products still look good because they have been given enough space now.
Amazing job! That's all and even more than what I planned for this chapter. So we've created the tenant shop page And we've created this and this is what we're going to do in the future. Now let's go ahead and push this to GitHub. So 16 tenant pages.
I'm going to go ahead and do a git checkout, b, 16 tenant pages. I will do git add, git commit, 16 tenant pages. And then git push uorigin 16 tenant pages. You should now see your branch change in the IDE as well. You should see the detachment from main or master branch.
Let's go to GitHub, let's open a pull request, and let's create a pull request. So, the summary. New features. We have introduced tenant-specific homepages featuring a refreshed layout with dynamic navigation and a new footer. We have also launched an enhanced product page that filters and displays items based on tenant context, adapting the view for different layouts, speaking of the narrow view.
We also added seamless URL generation and improved backend endpoints for retrieving the tenant data ensuring a smoother overall experience. As always, you can go ahead and read through the walkthrough. You can see how it used the prefetching via TRPC. So it noticed absolutely everything we did here. As always, some useful sequence diagrams here, which are the most interesting thing to me, because it's really important to understand what we have built.
So if you want, you can even pause your screen and you know, I'm going to zoom in for you so you can see. Just try and understand what's going on here, because these are really, really good diagrams here. And let's see if it left any comments. So these are potential issues. And you can see how it doesn't know Tailwind version 4 yet.
This is a unfortunately something that we're gonna have to work with with all AI models. I don't think a single one knows the new syntax. So yes, this is not correct. We are using the correct version. Don't worry about that.
In here they suggest some refactoring, but I'm okay with this as it is at the moment. To do skeleton for checkout button, yes, that's in to do. Fallback for missing tenant name, We don't actually need this because the navbar is in the use suspense. So we are certainly going to have the name. So we can merge the pull request and we can confirm merge.
I'm not going to delete the branch instead I'm just going to confirm that we have lesson 16 here and now as always we are going to check out to our main or master branch and we are going to pull origin master and then git status on your main or master branch to confirm that you are up to date. That's it, that is the chapter, amazing job and see you in the next one.