In this chapter, we're going to go ahead and develop the product page. This is what it's going to look like. We're going to start by building the get one product procedure so that we can actually get the data we will be working with. As always, ensure that you're on your master branch and ensure that you are up to date. I'm going to quickly do that using git status.
You can go ahead and do bun run dev after that. Then let's go ahead inside of source, modules, products, server, procedures. And above getMany I'm going to add a base procedure called getOne. I'm going to define the input here and I'm going to define the query. Let's do the input first.
Inside of this Z object here, we are only going to be looking at the ID. That's it. And then in the query here, let's go ahead and destructure the context and the input. And let's go ahead and define const product here to be await context database find by id collection products and simply pass the id as input dot id like this and for now you can just return the product We're going to fix the types later. Remember, we have the depth thing and the images.
So that's it for the base procedure. Now, I want to go ahead and create the actual product ID route. So we are going to go ahead inside of app folder, app tenants, home, and we're going to create products, and then product ID. So go ahead and create a page.tsx inside, And we're going to create an interface props here. And inside, we will be able to destructure two things from the promise, the product ID, but also the slug of the tenant.
Now let's export. Actually, we are doing a default. So let's define the props here. And in here we can destructure the params. We can already turn this into an asynchronous method.
Let's destructure asynchronous method. Let's destructure the product ID and the slug from params like this. And let's do void. Actually, let me just revisit the layout here in the tenants. That's where I did my most recent prefetch.
So I'm just going to do that here. It's a new syntax for me so I still don't remember it, right. And my original source code is of no help because it doesn't use the new syntax, right? Great, so we have this and now let's add the hydration boundary and let's add the state dehydrate query client so now we are refetching and what we have to do now is we have to create the actual page view here. So let's do product view and let's pass in the product ID, product ID and tenant slug to be slug.
Now let's go ahead and let's create the product view page. So we're going to do that inside of source, modules, products, UI, views. And I'm going to add product view.dsx Now inside of here, we're going to create the props which accept the productId and the tenant slug. And now let's go ahead and create the content of this. So export const product view.
We can go ahead and destructure the props from here. So product ID and tenant slug. And then we're gonna go ahead and start building the UI last name px4 on large px12 py10 Now let's go ahead and add another div here. And in here, let's add a border, rounded small, background white, and overflow hidden. And then In here, another div with a class name relative, aspect 3.9, and border bottom.
And in here, an image from next image, which is going to have a source of slash placeholder.png, which is the same thing we do in our product card. We use the placeholder.png, so just use the same thing here for now. Alt will be cover. Fill and class name object-cover. I think this is enough for us just to start seeing some results here.
So let's go inside of the page and let's import the product view from modules products UI views. Now in order to see this page, we also have to go back to our product card component and fix the href link. We're going to be using the generate tenant url with the tenant slug and then we're going to go to slash product slash id. So if you've done bun run dev you should be able to visit localhost 3000. And once you click on a product, you should now get redirected to this page.
So this is my URL. So you can see this is where I'm being redirected to localhost 3000 tenants the name of the tenant slash product and then the product id which is exactly what I've just put in the product card component So your URL should look like that, and you should be seeing this right here. So that's my structure, right? Let me just confirm with you. Instead of my app folder, app route group, I have tenants, the tenants, slug, home, product, product ID.
There we go. And now we can see what we are developing. So let's go ahead now and actually fetch the data here. We already pre-fetched, so all we have to do here is get use DRPC and then we have to get the data using use query actually use suspense query because we prefetched this so drpc.product get one query options and pass in product id trpc.products.get1.queryOptions and pass in productId.productId. And now we should most certainly have data at this point.
So what we can already do here is we can use data dot image question mark dot URL or placeholder dot PNG And we can also do data image, alt or data name. Actually, we should use data name here for the alt. Now we have the type error for the image. So we have to go inside of get one here and just modify the product here so I'm going to return the product and then we're gonna have an image which will be product dot image as a type of media or now We already have media imported from the types. So this works because the depth by default is 2, meaning that the image is populated.
If you change it to 0, it's not going to work. But if you change it to 2 or 1 it will work or if you don't add it the 2 is the default. There we go. So now what I want to do I mean you don't have any oh yes my apologies add use client here So I still see the placeholder because I didn't add any image here, but you can go to the admin dashboard and try. And also, by the way, yes, images are uploaded here because we didn't add any storage for payload so it's just keeping them here in the media folder I forgot to tell you about that sorry now Let's go back inside of the Product UI Views product view, and let's continue developing here.
So after the div encapsulating the image, let's go ahead and add a grid. Grid, grid columns 1, but on a large, grid columns 6. And then inside of here, let's add a div with a class name of col-span-4. And inside, let's add another div, which will simply be used for padding. And it's going to render a heading Data.Name.
And this heading will have a class name of Text for Excel and Form medium. There we go. John's product. Perfect. Now outside of this div, let's add a new div with a class name ex6py4 flex items center justify center and the border right and inside of here we're gonna add one more div relative px2 py1 border background pink 400 width fit and the paragraph Px2Py1BorderBackgroundPink400WidthPit And the paragraph Data.Price and the paragraph can have a class name of text space and font medium.
There we go. If you want to, you can go to the product card And you can extract this into a util, where we added the generate tenant URL. Let's export function format currency. Value number or string. Let's return new intlNumberFormat and pass in the value.
And I think this should work just fine. You can now use the format currency here. From libutils. I think this will come in handy. And you can do the same thing in the product card now.
Make sure you've imported it. There we go, looks great. And let's just confirm that it didn't break the card. It didn't. Still looks great.
Now let's continue developing the product view here. So we stop here, creating the box. And I don't think we need relative actually. It will look exactly the same without it. Now let's go one, two divs outside here.
And in here we're going to create a div which will render the author or the shop right the tenant so bx6 by4 flex items center justify center on large border on the right side and in here let's add a link and the href will simply be generate tenant URL from libutils and passing the author username. Let's see The author username. We actually don't have to do it like that. We can use the tenant slug since we have it. So yes, we can do that right here.
And let's also, yeah, let's extract two things, so it's easier. Const, the tenant image URL will be data. Actually, we don't need to extract. It's fine. Let's just go back here.
And let's continue developing. So now inside of this image, I'm also, inside of this link, my apologies, I'm also going to add a class name, flex-items-center-and-gap-2. And then I'm going to check if our, let's do, data tenant has an image question mark dot URL. Only then are we going to render that image. So let's give it a source of data tenant image URL, an alt of data tenant name, and let's go ahead and give it a width of 20, height of 20, and class name of rounded pool border ring zero and size of 20 pixels.
Now let's go ahead and let's resolve these typescript errors here so I'm gonna go instead of get one confirm that you have depth to here add a tenant which will be product tenant as tenant and image media or now in order for this to work, you need to have depth 2, because loading the tenant is depth 1. Loading the image of the tenant is depth 2. So this will load the product.image, product.tenant, and product.tenant.image, right? That's depth 2. You can now see my image here, but if I change it to depth 1, I shouldn't be able to see my image.
So you can keep it at depth 2 in this case. Now let's go back to the product view and you can see no more errors in here and inside of this link let's add a paragraph, data, tenant and let's showcase the name. Let's add a class name here text base, underline and font medium. There we go. I can now click here and I will be redirected to the user's shop Now besides the name, we're going to add the rating Keep in mind that we haven't implemented any rating yet.
But we will have. But we will do. Later. So outside of this link and outside of this div add a new div with a class name hidden meaning not visible on mobile but on large it's going to be flex let's add px6 py4 items center and justify center Now in here add a div with a class name flex items center and gap one. And then let's go ahead and create a star rating component.
So star rating, simply because we might, we will definitely reuse it. So let's go inside of source components, not inside of UI. Instead of UI, I want the reserve for the chat CN components, but star-rating.tsx will be our own component so we can keep it here. Let's go ahead and import two things and add two constants. So star icon, the CNUtil, maximum rating, and minimum rating.
Then let's go ahead and create an interface starRatingProps with rating as the number, optional class name, optional icon class name, and optional text. Then let's export const the star rating component, destructuring the props from above. Now let's go ahead and define safe rating using MathMax and MathMin, combining the minimum rating and maximum rating constants. And now we are ready to build a component. Let's create a div with class name CN flex items center gap x1 and whatever optional class name might come here later.
And now we're going to create an array from length maximum rating, in this case 5. Let's map over the items, skipping the first one and just using the index. If you want to, you can also do this. Actually, you can't. My bad.
Now in here, render the star icon. Give it a key of index and a class name. CN, size 4 by default. And then conditionally, If index is less than safe rating, go ahead and give it fill black, otherwise an empty string and icon class name here. And make sure to add a comma after this.
And then outside of this, add a text and a paragraph text, like this. That's it. That's our star rating component. Now we can go back inside of the product view component and we can now import the star rating component from components star rating. Let's go ahead and add some props to it now.
Since we don't have any values here, what I'm going to do is I'm going to give it a rating of three and icon class name will be size four. There we go. If you change it to five, it will highlight five. There we go. Perfect.
And maybe later you can even use it here if you want to. For now, let's leave it like this. So this star rating is specifically for desktop devices. What I want to do now is I want to go to the end of this div, border-wide flex. You can see I'm going to follow the line here to find where the div ends right here and I'm gonna go ahead now and create a new block which will be mobile only so block on mobile but when it hits the large breakpoint, I'm going to hide it.
I'm going to give it Px6, Py4, ItemsCenter, JustifyCenter, and BorderBottom. And Then I'm going to add a div here with a class name, flex-items-center, and gap-1. I'm going to copy the star rating from above here, like this. And then Beside, I'm going to add, for example, five ratings. And give this a class name, text base, and font medium.
Obviously, we're going to go ahead and change these later with real data. And now, let's go ahead to the end of this block element here and add a new div with a class name padding 6 and simply check if we have description go ahead and render the paragraph with data description this will later be rich text element but for now I'm just going to keep it as a paragraph. And if we don't have description, we're going to render no description provided. And we're going to have special styling for that. Font medium, text muted, foreground, and italic.
So let's just confirm that this is working. I have my description, so my description is loaded here. And you can see on mobile, you should have a different view. There we go. You can see the ratings are below that element, but on desktop, they are all in one column here.
And now we are going to finally develop this column right here. So go ahead and find where column span 4 ends. Looks like it ends right here with three divs left. And now go ahead and create a new div here with a class name column span to go ahead and create a div with flex, flex column, gap 4, padding 6 and border bottom. And then one more div.
Flex, flex, row, item, center and gap 2. Inside of here we are going to render a button from chat-cn-ui and this button will be used to add item to cart so let's go ahead and say add to cart this will also later be dynamic if it's in cart it will say remove from cart or if it's purchased, it will say view in library. Let's give it a variant of elevated. Let's give it a class name of flex1 and background pink or 100. There we go.
And now let's go ahead and besides that button, add one more button. This button will have a class name size 12, variant of elevated, onclick, and disabled, explicitly false. And for Now let's render the link icon. You can import link icon from Lucid React. Now you should have a button next to it, which we are going to use to copy the current URL.
Outside of this div encapsulating those buttons, Go ahead and add a paragraph with a class name, text center, and font medium. Inside of here, check if data refund policy is equal to no refunds. If it is, render the text no refunds. Otherwise, open backdicks, render data refund policy, along with the text money back guarantee. This will render 30 day money back guarantee or whatever you have set for the product.
And now let's go ahead and do the final component here. So outside of this div, add a new div with the class name padding-6. Outside my apologies, inside add flex-items-center and justify-between. Add an h3 element, ratings, with the class name, text, extra large and font medium. There we go.
And now, I'm going to go ahead below this and create a div with the class name flex item center gap x1 and font medium. We're going to use the star icon which you can also import from Lucid React, and give it a class name of size 4 and fill black. Below that, I'm going to, in parentheses, render the number 5. And then, I'm going to go ahead and render again five ratings. I'm going to add a class name here, text base.
At the top of this entire file. I'm going to add to do add real ratings And I'm also going to do the same in my product card. I want to do this just in case so I don't forget it later Great So we now have the ratings here, and you should be able to see them like this. And now what we're going to do is we're going to create a grid of progress bars indicating which percentage of customers gave what rating. So let's go ahead and give this a class name of grid grid columns inside of square brackets out though underscore 1f air underscore out though gap 3 and margin top of 3 and now inside of here let's go ahead and do the following.
Let's go ahead and open curly brackets, open square brackets, 5, 4, 3, 2, 1, .map. Get the stars inside, add a fragment. You can import fragment from React. The reason we are using the fragment as an element instead of just using this is because I want to pass in the key prop. So stars, my apologies, this will be, yeah, stars.
And then we will do a div with a class name, font medium, and render stars. Or we can do this. Actually do this right star is equal to one. In that case, the text next to this number will be star, otherwise stars. Let's refresh and see.
There we go. So five star, one star, four star, two star, three stars. Let me just confirm that my grid columns is correct. It is, but we are missing something here. So after this div, add a progress element from components UI progress.
We have modified this component early on in the tutorial, so it matches the NeoBrutalism style, which we will use. Let's go ahead and give it a few props. Value, which I'm going to put to be... You can put it to 0, you can put it to 5, whatever you want, because you don't have real data yet. But give it a class name of Height1LH.
And below the progress, go ahead and create a div. And in here, you can put 0 and then percent and give the class name a font medium. And there we go. Now you can see how this distribution grid is going to look like. If you want to, you can go ahead and give it some values, just so you can see how it's going to look like in the future.
So we don't yet have any review data, but this is how it's going to look like later. That's it for the UI part of the product view component. So if you take a look at this picture, we have pretty much written everything here. And in the next chapter, we're going to introduce ratings so we can finally see some real values here. And then we will have enough material to start implementing these buttons right here.
None of these actually do anything right now. And also, one quick tip. If you want to, you know, you can go inside of products, collections, collections, products, and nothing is stopping you from adding another type of image, right? If you want to, you can add a cover, like this. And then use that here instead.
Let me show you how that would look like. For example, you could go... Let me see, inside of product view... Find this, and just use data.cover.url. And you would also have to go inside of get one here and modify this to be cover, product.cover as media or null.
So if you want to, you can have two separate images. One for the cover and one for the card, right? And let me just see what is going on here. I think this is just MongoDB. Yes, no problem at all.
So if you want to, you can use two images, right? I prefer using one. So I'm going to remove these changes now, but I just wanted to give you an idea of how that is going to look like. Great! So let's see if we did everything we planned.
We added the get one procedure, we created the product component and we put it in this spot right here. And we have modified the product card link href. Now let's push this to GitHub. So 17 product page. I'm going to go ahead and do git checkout-b 17 product page.
Git add git commit 17 product page and git push you origin 17 product page After you confirm that you are on a new branch here and you can see your detachment, let's go ahead and merge that by visiting our GitHub and opening a pull request. Let's see what our reviewer has to say. And here we have our summary. We have launched a responsive product view displaying detailed product information, including images, descriptions, pricing, tenant details, ratings, and refund policy messaging. Of course, we can see the walkthrough of each change file by file and the sequence diagram which is describing our product view and how prefetching works.
So you can see that when user visits the page component we resolve the params promise so we can continue going forward. After that, we initialize the query client. Once we have the query client, we can finally prefetch the data, and then we can pass that cached data to hydration boundary. And then hydration boundary, which renders the product view component, can query product using the suspense. Amazing diagram here and let's see if it has any comments here.
So refactor suggestions, new get one procedure to fetch a single product. You can see it looks good, but it's lacking some errors. So that's definitely something we should improve. Yes, this is cool. So I removed this line because I didn't need it, but you can see how it recognized it because I forgot to remove cover image from my collection, actually.
In here, of course, it noticed the hard-coded values. That's something we're going to fix later. In here, it also noticed unused onClick. And yeah, this is exactly what we are going to do we're going to copy the location href but I'm satisfied with what we have so I'm going to merge this branch 17 product page and as always I'm just going to confirm that my branches look correct. So let's see.
010345678910. There we go. 17 product page. I could have sworn that in my previous videos it was the opposite direction. Perhaps after a certain number the branches changed direction.
I was already scared something happened. Great. So I have 17 product page here. I have merged it and now I'm going to check out to my main or master branch and get pull origin master or main after that git status to confirm everything is up to date and you can also confirm with your graph here and your IDE that you are on the main branch. And that concludes the product page chapter with Push2Github.
Amazing job and see you