In this chapter, we're going to go ahead and develop the review's UI and procedures. We're going to start by fixing the invalid checkout instead of our success redirect when the user purchases something. As always, ensure that you are on your master branch and that you are up to date. After that, you can go ahead and run bun run dev. Let's go inside of checkout view and in here, in useEffect, when we do router.push if state is success we shouldn't go to products we should go to library and let's also invalidate our library getMany So if a new product is purchased, we have to refetch that.
So we can do that by adding const queryClient, use queryClient from 10-stack react query and once you have the query client whoops inside of here you can do query client invalidate queries and pass in TRPC dot library get many and let's add infinite query filter here and now inside of our dependency array here we have to to add QueryClient, we have to add the RPC library.getMany() method like this. And now every time a success has been detected in the checkout, it will automatically refetch the library.getMany() and then it's going to push. So if you want to, you can also put an await here, but since this is not an asynchronous method, I don't think it will work that way. So you can just do it like this. So that marks this as complete.
Now let's create the getOne library procedure. So instead of library, server, procedures, we already have getMany. So what I'm going to do is I'm going to copy getMany and I'm going to rename it to getOne. It will be a protected procedure which just accepts the product ID. And then inside of here we are going to check limit to one, pagination to false, remove the depth, remove page and limit.
And inside of where we're gonna add and. The first one will check if the product equals input product ID. The second one will check if user equals context session user ID. So we are confirming that the user trying to load this library get one is the person who has the order meaning the person that purchased this ID. And then what we are going to do is the following.
We can get the order by simply going to orders data first in the array like this. If there is no order we can throw new trpc error with the code not found and you can import the TRPC error from at TRPC slash server. If you want to you can add a more descriptive message order not found And then you can go ahead and find the individual product using context database find by ID. Collection products where ID is input product ID. When it comes to findById, there is no where, it's just ID.
My apologies. And you can just return the product. So a simpler method than getMany, but still, it should handle everything that we need here. And if you want, you can also handle if there is no product here to throw a new ERPC error. Code not found and a message product not found.
There we go. Now that we have this procedure, we can go ahead and develop the library product ID UI. So let's go inside of our app folder, app, library. Instead of the library, create product ID. And inside, you can copy and paste the page.
Now we're just going to modify it a little bit by giving this one an interface with params, which are a promise, which hold the product ID, which is a string. So always make sure that you don't misspell, right? It's case sensitive. Otherwise, your params are always going to be undefined. From here we can destructure the params and then we can await them and destruct product ID from await params.
We are not going to prefetch infinite query, we are just going to prefetch a normal library get1 using normal query options. There will be no limit, instead, there will be a productId. Like this. So, no need for this one. And now, instead of LibraryView, we're going to use ProductView, but make sure you don't import one, because we first have to develop it.
Let's do that inside of modules, library, UI views. You can copy the library view and paste it, and rename it to product view. Rename the constant to product view here, and create an interface, props, product ID, string make sure you don't misspell it then go ahead and assign the props here and the structure the product ID instead of continue shopping here we're going to say back to library here. And for the header, we're going to do something different. So it's okay, We now have the product view, which means we can go back inside of this page where we do library get one prefetch and import product view from modules library and pass in the product ID here.
There we go. Now let's continue developing inside of the product view. So inside of here, what we are going to do is we are going to destructure the data and call useSuspenseQuery from tanstack react query. And we are also going to prepare the RPC from use the RPC on the RPC client and passing the RPC dot library get one query options and inside pass the product ID and since this is suspense you are guaranteed to have this data. So instead of library here, you can add data dot name here.
You can remove the lower one and you can remove the gap and plex classes. And in here, you can remove this as well. So you don't need product list or suspense here at the moment. So you should be able to now go inside of a library. And you should be able to, oh, yes, we forgot to mark the product view as client component.
So let's wait for that and there we go. This is still not working correctly. So if you go to the library now, you should be able to click on a product and it should load the product. But if you click on the back to library, it should go back to a library. If your library is not redirecting to this page, it most likely means that inside of the library view, inside of the product list, product card, you didn't have a proper redirect here.
And you can also add a prefetch here, actually. So it's going to be even faster in production. So now we're going to focus on this page right here, which is the product view instead of the modules library UI views, inside of the section. Inside of the section, we are going to create a grid holding two columns. So let's create some space for that.
Grid, grid columns one on the mobile, but on desktop, it will be grid columns seven, gap four, and on large, gap 16. Now inside, we're going to have two columns like this. The first one is going to be LG column span 2 and the bottom one will be LG column span 5. Inside of the first one go ahead and add a div with a class name of padding4, background white, rounded medium, border gap4, and let's leave it like this for now. And inside add to do review sidebar.
Inside of lg-colspan5, we are going to go ahead and simply render a paragraph, no special content, and give the paragraph a class name of font medium italic and text muted foreground. So this is how it's going to look like now. And later, we're going to have some special content in form of a rich text element, which will also include file uploads. So the author of this product will be able to share whatever they want with us. A file, a PDF, a link, a license key, whatever, right?
Now let's go ahead and focus on reviewing on the review sidebar here. So in order to do the review sidebar, we first have to collect the reviews collection. So let's go inside of source, inside of collections, and let's create reviews.ts. And we can copy categories, for example, just so we speed things up. So let's rename this to reviews, slug the reviews as well and now We're going to go ahead and modify this a little bit.
It's not going to have a name, but it will have a description. And the type will be text area. It's also going to be required. And then it's going to have a rating. Rating will be a type of number, which will be required.
And it will have a minimum of one and a maximum of five. After that, we're going to have a relation with the product, which is a relation to product, has many set to false and required will be true. And using the same logic, we are going to have a related user that created the review. There we go. That's everything there is about the reviews.
And we can use the description as name in this case. Now let's go inside of payload.config, and let's ensure that we add the reviews here. There we go. Now that we have the reviews collection, let's go ahead and let's create the reviews procedures. So I'm going to go inside of source, modules, and I will create a new folder, reviews.
And inside of here, server, and then inside procedures, whoops, procedures.ts. To speed things up, we can look at tags, for example, and we can add them inside of reviews procedures like this. Let's go ahead and rename this to reviews router. Let's go inside of the RPC underscore app inside of the Routers folder here. And then in here, we should be able to import the ReviewsRouter like this.
And we should be able to add reviews to the reviews router. There we go. And now instead of the reviews router let's add everything we need. So we're going to need get one which is going to be a protected procedure. Instead of the object we are only going to aim at the product ID for which we want the procedure.
And inside of here, we are going to get the product using await context find by ID. The collection will be products. And we are just going to pass the ID to the input product ID. If there is no product found, we are going to throw new TRPC error here with the code not found and a message product not found. Make sure to import the TRPC error from trpc server.
This is a module, so I'm going to move it up there. Once we know that we have the product, we are going to get the reviews data here using await context database find collection reviews where and let's also ensure that we have a limit of one and and add two options as always if you have typescript errors when loading reviews you're gonna have to run generate types. Now inside of here let's do product equals input product ID or we can do product ID like this and the user equals context session user ID. There we go. That is how we fetch the reviews.
Now let's get a single review from here. Reviews, data, docs first in the array. If there is no review, we're just going to return null. Because we don't want to throw errors if there is no review. That's okay.
But if the product was not found, well, that's an error. But if there's no review, that's fine. There's no need to have a review now let's go ahead and let's create create which is a protected procedure and let's go ahead and give it the following input We need a Z object with the product ID for which we are creating a review, a rating, which is a number where the minimum is 1 and the message is rating is required and the maximum is 5. And a description where the minimum is 1 and description is required. If you want to, you can also change this to three.
Great. Now that we have that, let's go ahead and let's add the mutation after the input. And inside of this mutation here, What we're going to have to do is first find the product using context database find by ID, so the simpler one. As always, if the product wasn't found, we're going to throw a new trpc error with not found and product not found here. What we have to check for now is there an existing review.
So const, we're going to do existing reviews data to be await context database.find collection reviews dot find collection reviews where and and in here product is going to be equals input product ID and user equals context session user ID like this. And then what we're going to do is if existing reviews data total documents are more than zero that means that we've already created a review for this product. So what we're going to do instead is throw a trpc error, you have already reviewed this product. Otherwise, Let's create a new review using await context database create, collection reviews, and data. The user will be the currently logged in user.
Product will be product ID. Rating will be input rating. And description will be input description. As simple as that. And let's return the newly created review.
Now we can go ahead and copy our create procedure here. And then we can change it to update. So it's going to be quite similar with an exception of instead of product ID having a review ID. So the beginning will be a little bit different. Instead of looking for an existing product, we will look for an existing review using context.elevate.findById, change the depth to 0, collection will be reviews, and id will be input.reviewId.
So now we're going to check if there is no existing review, we're just going to throw the error, meaning that you can't update a review that doesn't exist. And now another thing that we need to check here is the following. So we set the depth to zero, which means existingReview.user will be the user ID. In our case, that's exactly what we need. So just do if existing review.user does not match context session user ID, you're not allowed to update this review.
And the rest is pretty simple. So let me remove this, remove the everything until we do create. And instead of create, it will be update. So we need ID to be input review ID. And the only thing that we are going to allow to be updated is the rating and the description.
And let's call this updated review. There we go. We now have our procedures to update. We have the procedure to create and the procedure to get the reviews. Great.
So we can now head back to our product view inside of the library module. And in here we have added a to-do, ReviewSidebar. Let's go ahead and create it, ReviewSidebar. Now let's go ahead and add the product ID here, product ID. Now I'm gonna go inside of components and create the review sidebar.tsx.
The reason I'm creating it here is because it's going to be tightly coupled with library rather than the reviews itself. So instead of the review sidebar, let's create the props which accept the product ID. And now let's go ahead and export const review sidebar. And inside of here we can just destructure our props like this. And inside, just return ReviewSidebarText, like this.
And now you can actually import the ReviewSidebar, and you should no longer have errors here. So nothing much to change except it should now say review sidebar. Now inside of this review sidebar what we are going to do is we're going to fetch the review here. So let's go ahead and get the TRPC from useTRPC. Let's go ahead and get the actual data here from useQuery.
My apologies, useQuery. Yes, I thought, never mind. So instead of useQuery, trpc.library, getOne, queryOptions, and inside, productId. There we go. And it's not going to be useQuery, useSuspenseQuery.
I knew something was off. There we go. So useSuspenseQuery like this. Whoops. UseSuspenseQuery.
There we go. And what we can do here is just for now JSON stringify data null and number two. So right now inside of here, this is interesting because I get a review here, but I shouldn't. That's because I'm using trpc library get one what I should be doing is reviews get one so we are fetching for one review here so after you refresh you should see null here because we don't have any now instead of here we are getting an error here, not authenticated. That's because we are attempting to use useSuspenseQuery without prefetching it.
So what we have to do is go back inside of our app folder, library, product ID page. And besides prefetching the library get one, let's also prefetch refuse get one based on the product ID. And now, as you see, no more errors. So that's one gotcha, you know, as we would say that you have to remember every single time you use useSuspenseQuery, you need to have a matching prefetch. And if you use useSuspenseInfiniteQuery, you need to have a matching prefetch.
And if you use useSuspenseInfiniteQuery, you need to have a matching prefetchInfiniteQuery. These things are very important. And the React Query and 10-stack team is actually working on a couple of strict wrappers, which will alert you in the future if you are forgetting to do that. Right now you just get this cryptic errors which say unauthorized. It's very cryptic.
Like, would you have guessed that it's because of a missing prefetch? I don't really think so, right? But for now, just keep in mind, every single time you do useSuspenseQuery or useSuspenseInfiniteQuery, you need to have a matching prefetch. And if you're just using useQuery, then you don't. In that case, it's completely fine.
Great, so in here it says null. And now what we have to do here is return a ReviewForm component. Instead of here, we're going to have ProductId. And we're going to have the initial data which will be the data. And now let's go ahead and create the Review component inside of components.
Review form.tsx. Let's start by creating an interface, review get one output. I'm sorry, we don't have this yet. So let's quickly create the review get one output by going inside of our reviews types.ts. And what I'm going to do now, let me just save my review form in this state, what I want to do is I just want to visit my category modules because I already have the types here so I will just copy them and paste them in my reviews and in here I am looking for export type reviews, get one output.
So this will be reviews, get one. That's what I'm looking for. And now I'm going to go back inside of library UI components review form, and I will be able to import this from modules reviews type. There we go. Because the place where we render our review form is inside of the review sidebar.
And this is the exact type that we are using. And by the way, we can now import. Actually, we can't yet. My apologies. So let's go ahead and export const review form.
And let's return a div review form. And now you should be able to import the ReviewForm from ./.reviewform. Not much should change, just the text to review form. But now let's go ahead and import everything we are going to need to make this form come to life. Whoops.
Okay, let's go inside of the review form here, and let's import everything we are going to need to build the form. Zod, useState, useForm, and ZodResolver. And then from our alias imports, the RPC from the RPCClient, button from componentsUIButton, and this is not true. This will be use TRPC. Text area.
And the star picker is a component which we are going to have to develop. So I will comment it out for now. And that will be it. So let's go ahead and start building until we get to the star picker, at which point we're going to have to develop it. So our form schema will look like this, rating, a number, which is required, and a description.
So similarly to how it looks like in the DRPC procedure for our reviews. So besides the product ID, these two should be identical in the form schema. If you want to, you can reuse them. Great. So once we have the form schema what we can do here is the following.
We can set the form to be useForm and use zInfer type of form schema. Use the Zod resolver on the form schema and set the default values to first check if we have the initial data. If we have initial data dot rating it's going to use the rating otherwise fall back to zero as default and the description will be the same thing. Description or fallback to an empty string. And one thing you can do here is detect if this will be an edit or a creation of a new review based on the initial data.
So I'm going to add a state which uses a boolean of the initial data to mark isPreview to true if we receive initial data. So isPreview basically means, okay, we are previewing an existing review. Great. So now that we have this, let's also prepare a const onSubmit method here, like this. And inside of here, let's add data z infer type of form schema and do a console log of the data for now and now instead of here let's use the form let's spread the form like this Inside a normal HTML form element with a class name of flex, flex column, ngap y4, and an on submit method of form handle submit and pass in the on submit which we have developed here.
Great. Now inside of here I'm going to add a class name fontMedium which depending on if this is a preview, will say your rating, or liked it, give it a rating. So we are either telling the user to edit their rating, or to create a rating for the very first time. So now, what we're going to do below this, and let me just see. I think I completely forgot to import my form elements.
So yes, we need all form elements, control, field, item, and message from components UI form. So just let's get that ready. So what we're going to do now is below this, create a form field. And the form field will have a control of form.control. Let me just fix the typo.
The name will be description which is basically the field that this form is controlling and inside of the render we're going to go ahead and structure the field and inside we're going to return the form item form control and text area The text area will actually be a self-closing tag like this. Give it a placeholder of want to leave a written review. And disabled will be is preview. And then just spread the field and below formControl add formMessage which is a self-closing tag like this And then just below the form field here, go ahead and check if not is preview. In that case, Render a button.
The button will have a variant of elevated, disabled of explicitly false for now, type of submit, size of large, class name, background black, text white, hover background pink 400, hover text primary, and width fit. Background-black, text-white, cover-background-pink-400, cover-text-primary, and width-fit. And it's going to check if we have initial data. In that case, it will be update review, otherwise, post review. Great.
Outside of the form element, do the opposite of is not preview. In here, it will be if is preview. So if we are in the preview mode, we're going to have a button again which will say edit. So what this will do on click is modify the set is preview to false so that the user can now go ahead and edit their preview. Give it a size of large, a type of button, this is quite important, and a variant of elevated and the class name with fit, like this.
So now, this is how it should look like. So since we don't have an existing review, this is how the first time review field looks like. One thing that's missing is the star picker component. So let's go ahead and let's create the star picker component quickly. So I'm going to go inside of source components.
We have star rating, but now we're going to do star picker.psx. Let's mark it as use client, import use state, star icon from Lucid React, and CNUtil from libutils. Let's create an interface star picker props with an optional value, which is a type of number, optional on change, which accepts the number as its value, optional disabled, and optional class name. So a highly reusable star picker component. And now let's go ahead and actually export the constant star picker.
Now let's make use of all of these props here, starting with the hover value. Implement hover value and set hover value using useState which we have imported and set the default value to 0. And in here, let's go ahead and return a div. Give the div a dynamic class name using CN. By default, flex and items center.
If disabled, give it an opacity 50 and cursor not allowed and give it a class name as well. And then we are going to spread an array of one, two, three, four, five elements inside, do a math and call the prop inside a star and then use a native HTML button here. Go ahead and give it a key of star, a type of button, disabled of disabled, and again a dynamic class name using CN. On padding 0.5, hover, scale 110, increase, transition. And if it is not disabled, it is going to have a cursor pointer.
So if not disabled. On click here we are going to call on change with a question mark because it's optional and pass in the current star that the user has clicked on. On mouse enter we are going to set hover value to the current star user is hovering. But on mouse leave we are going to reset it back to zero. And finally inside render the star icon.
Inside of here, add a dynamic class name with size 5 as default, and then if we have hover value or if we have value and it is larger or equal than the current star level, give the class fill black and stroke black. Otherwise just stroke black. And that's it. That's our component for the star picker. What we can do now is we can go back instead of the review form.
And what I'm going to do now is import the star picker from components star picker. Because that's where we created it, right? Instead of components, star picker. And now what I'm going to do is I'm going to copy this entire form field and above it I'm going to paste it. So you should now have two text areas.
But we're going to change the first one with name rating. And what we're going to do is we're going to use our star picker component. Like this. Go ahead and pass in the value of fill.value onChange to fill.onChange and disabled is preview. So now you should be able to give a rating with a cool effect.
But we can't just do that yet. So if I go ahead and select 4 and... Oh, it looks like it doesn't really work. I might have forgotten something. Let's see, star picker, we do have on change, right?
We do have on click here. But fill.value does not seem to stay. So what I'm just going to do is go inside of star picker here. And I first want to confirm that onChange is being called. So I'm going to do const handle change here.
I'm going to get the value which is a type of number here and I will console.log the value and after that I will call onChange with the question mark and pass the value. So modify on change to call handle change and no need for a question mark this time. This is okay. Okay. So let's see.
I will do one refresh here and I will click on number four, and I can see value four here and looks like now it works properly. I just needed to refresh my page. So it stays on the number I click. So I'm going to remove this. And now, if I, for example, select three stars and leave test and click Post Review, you can see that I have a rating of three and description of text, which means that this is ready to be submitted.
And lucky for us, we have already implemented all the mutations we need. So let's prepare using const drpc use drpc. And let's create the mutation called createReview, which will be useMutation from tanstack react-query. So let me just move it here. There we go.
The useMutation will accept the RPC, reviews, create, mutation options inside. And at the moment on success will be nothing, we're just going to create it and on error will also be nothing, We're just going to create it. And you can do the same thing for update review, which will use update. Great. So now let's go ahead and modify the onSubmit method to check.
If we have initial data, we are going to call updateReview, mutate and pass in the reviewId to be initialDataId, rating to be dataRating, and description to be dataDescription. Else, we are going to call create review for the first time with product ID, rating data dot rating and description data description. Perhaps we should call this values and then just do values values. There we go. So now we have a proper on submit here.
But we're still not done. What I want to do is also add query client, use query client. You can also import this from 10 stack React query. And now what I want to do every time that we create something here on success, I will do query client, invalidate query. And inside, I will do trpc.reviews.get1 query options and passing product ID like this.
And also do set is preview set to true. So it kind of locks it, right? And on error, we can extract the error here and do toast from sonar error, error.message. So toast from Sonar. And you can copy the exact same things for the update review.
So the same one will be invalidated. And set is preview set back to true. So now I'm gonna go ahead and try this. So I will select four stars and I will say awesome product and I will click post review and there we go. Your rating is right is right here.
One thing that seems to not work correctly, at least I think, is this button Edit. It's too flush with my description here, so I'm just looking for a smart way to move it. I think what I can just do is give it an empty of four this way. And then if you go back to the library and go back here, it should look like this, right? Oh, this is cool.
It still does the effect. So you can go inside of a star picker here and on disabled hover Scale 100. So it doesn't do any effect. Let me refresh. Looks like it still does the effect.
Scale none. OK. Not sure what it is. We can leave it with VFX for now. But okay, yeah.
I think this now works, right? So I can edit and I can change it to three and say okay product and click update review and there we go no errors it works great one thing that's missing is for this to be disabled if we are calling createReview.isPending or if we are calling updateReview.isPending. There we go. So now it should look even better. You can see how it disables it, right?
Great! You can see that now inside of here. And since we are not protecting the CMS yet, you should also be able to go to your dashboard here and you should be able to see that review. So let's just wait for the dashboard to load and go inside of reviews. And there we go.
Description, okay product with a rating of 5 and the user is John. We don't have access to the product because we haven't created that product. Later, only super admins will be able to see this screen. The tenants will only be able to see the products. Great.
So this seems to be all working. What we're going to focus on in the next chapter is some of the last UI elements that we are going to do, which is basically the calculation of the rating for the display in these cards here and the distribution right here. So it's not going to be too difficult because we have all the information we need already. And then we're going to slowly wrap up the project by implementing the fee sharing functionality using Stripe Connect and implementing the middleware rewriting system for subdomains. But the project is already coming together quite nicely, right?
We can view it in the library from here now, we can see how good that looks as well. I'm very satisfied with it. And of course, we need to add the special content, right? All right, now we don't have the logic for that. Don't worry, we're going to do that as well.
So we have created the review procedures and created a review form UI. So that's 22 reviews. I'm going to go git checkout-b22-reviews, git add, git commit 22-reviews, and git push uorigin 22-reviews. There we go. Once you've confirmed you are on the new branch and you can see the detachment here, you can go inside of your ecommerce here and go ahead and create a pull request.
And once you've created your pull request, we're gonna go ahead and review our changes. And here we have the walkthrough of our changes. So this set of changes introduces several new UI components and the back-end procedures that enhance the product and review functionalities. A new asynchronous product page prefetches product data and displays it using hydrated component. New review components and a reviews Collection are established for submitting and managing user reviews.
Additional server methods are added for robust error handling when fetching products and reviews. Minor improvements in the checkout view and the product card help with query invalidation and prefetching. And finally, payload configuration changes and TRPC routers have been updated to integrate the reviews functionality. That is exactly what we worked on. As always, if you are interested in these sequence diagrams, which I always find fascinating, You can pause the screen to take a look here.
So in here it's describing the fetching of the product and the review data and in here it is describing how our submit form works depending on create or update. So quite interesting to verify after a pull request. So I'm satisfied with the changes. In here, it added an action to add a brief comment. So it's pretty cool that sometimes it doesn't really need a change it just thinks you should clarify things in your code but I am content with how I've built it at the time so I'm going to leave it like this same for these other changes.
So I'm going to go ahead and merge my pull request. I'm not going to delete it. Instead, as always, I'm going to confirm that everything here is in order and then I'm gonna go back to my branch. I'm going to pull origin into my branch and do git status to confirm. And I can see my graph changes here as well.
There we go. That's it. We have implemented reviews. What we have to do next is we have to aggregate those reviews into product cards and product views. Amazing job and see you in the next chapter.