Full Stack E-Commerce App with Next 13, React, Sanity, Stripe, & TailwindCSS

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in this full stack project you will learn how to build and deploy an impressive e-commerce app made with next 13 sanity and Stripe from start to finish this project has every major feature you would expect from a modern e-commerce app such as an attractive and accessible user interface a powerful shopping cart where customers can add update and remove items from their cart the ability to sort products by different criteria such as price and date and advanced filter component that enables users to select products according to properties like category size and color as well as a full text search to find any product with ease customers will be able to choose products from a range of sizes and view different product images through a custom image gallery that you'll create for each item and when customers are ready to check out they can review and manage items in their cart see and change the running total with shipping costs and then purchase their items through our own custom stripe checkout page and to top it off our app will have a fully responsive design for any size device along the way you'll learn a ton of core next 13 features such as react server components with lots of data fetching patterns as well as route handlers for server-side requests you'll see how to manage your product data within your application using sanity with your own custom Studio as well as how to describe data through schemas and query that data using the grok query language after that you'll learn how to charge customers for their products with stripe using stripe checkout add shipping costs send invoices and retrieve information from customers purchases and finally when our app is ready you'll push your app to the web using versel to get started with this project the only thing you need to do is click the link in the description to grab the starting code not only does it include all the basic code and styles you need it also features links to all the tools we'll be using plus all the essential dependencies you'll need to build our app from start to finish so pause this video grab the starting code and let's get started once you've downloaded the starting code for our app you can grab the app folder with all of our starting code and drag it into your code editor from there we can open up the terminal and install our dependencies with npm I and when that's done we can run our project with npm run Dev once it starts up on localhost 3000 we can open it up and at the moment on our index page we don't really have anything at least not compared to the final version of our app a good place to start for us is going to be in the root layout component this is within the app folder and that first layout file you see is the root layout by default it wraps all of our Pages this is a good place to add things like metadata as well as any providers that we need right now we see that we don't have a title or any metadata really from the layout file if you export a variable named metadata we can add a bunch of our standard metadata values as properties on an object so for title we can set that to the name of our app which is flare and when we save we see the title element in our head is being updated instead of using this hard-coded value you can head to the config folder and you'll see this site.ts file where we have a bunch of shared values like the site name description and links for the footer here Insight config we'll call we'll set the name property through the value flare and for the description this is a site for expertly designed Goods for the workspace home and travel if we save that and head back to layout we can now just use the value site config .name we can do the same for the description which will be set to siteconfig.description there's also an icons object we can see information about it with the help of this metadata type from next icons is an object where we can set the favicon using the icon property within it to just slash favicon.ico and if you look at your public folder which is what this is reading from you'll see that there is a file that will serve as our favicon right here and with that done we have some basic metadata and now's a good point to structure the layout of our application not only the user interface and what's going to be shared across each page if we look at the final version of our app we see that that is going to be a header as well as a footer no matter what page we go to this is visible but if we turn to dark mode we can better see that there's this colorful blob in the background as well if we find our first div in layout you'll see that it wraps some children which is all the contents of the page itself whatever page that we're on those three components that we identified will be around the page content the site header will be up at the very top the site footer will be at the bottom and the site blob will be right underneath the header and if we save the basic appearance of our app is now shaping up however none of the functionality is there just yet a lot of that is going to be included with the help of a special provider's component which will wrap around this div so it wraps all of the page content and all of these components this is important for react context to work properly which is what we're using when we add a number of different providers here within this provider's component and just one note about all the components that we've been using they've already been included up at the top of our file you'll see that we're importing from a components folder all of the different components that we've been using you can see that components folder adjacent to our app folder if you open that up you can see all of the different components that we're going to be using in order to build out our app so now we'll head to the providers component all of these providers are going to wrap the children that are received between the opening and closing tags here that is all of the page content and all of the components here we're going to set up all the providers that we have above we're going to have a cart Provider from use shopping cart to help us add shopping cart functionality we'll have this toaster component that will allow us to show toast notifications when a user performs a given action like adding or removing an item from their cart this Tailwind indicator is a really neat one that allows us to figure out the different break points we're on because we're using Tailwind to style everything and we also have a theme provider that allows us to toggle between dark mode and light mode we have a fragment here wrapping our children and we'll start with the the first one which will be our cart provider so we want an opening and closing tag for our cart we want to set the currency using a currency code I'll be using US dollars or USD I'll add the prop should persist so that it persists our state and local storage We'll add a cart mode of checkout session we'll see how to create a checkout session with stripe to allow users to check out the items that they've added to their cart and finally We'll add a stripe public API key now we don't have that just yet but it's going to come from our EnV file so if we head to dot EnV in the root of our project we'll see that there is a next public stripe public key and we're just going to copy the name of this environment variable and insert it here we want to make sure that we include process.env and then include the name of our environment variable and then we can add just an exclamation point to avoid the type error then within the cart provider we're going to wrap our providers around each other we're then going to have the theme provider with the attribute class which is necessary for it to work with Tailwind set default theme to system so whether it's light or dark mode is going to be according to the user's preferences and we want to use the enable system prop then above children but within the theme provider we're going to include toaster which won't have a closing tag so it's going to be a self-closing component and then we'll include Tailwind indicator underneath it so that's it for our providers we can save everything there we can save layout and the next step will be to move on to the home page and to see how to integrate sanity with our app to be able to more easily manage and display our store's products to get started with sanity we'll head to sanity.io and to create a new account you can just click on the start building button you'll be given some steps to set up sanity Studio however I've already configured that for you we just need two values that are included in this command that they've given us to run in our terminal you just need to grab the project ID you can also grab this from the URL which you can add to the dot EnV file and add it to next public sanity project ID and you want to include the data set production for next public sanity data set to set that environment variable to the value production if you were to go through the entire process of setting up sanity it would create all of the different files that you see here in the app folder it would create a folder called studio with a page the studio is where we're going to be able to work with sanity and work with all of our different products it's going to create a sanity folder with a lib folder and some different utilities that are helpful for working with nexjs and it would also create this dot EnV file so once you have your project created we can head to our terminal and we can restart our development server to load those two new values and we can head to localhost 3000 slash Studio when we do that for the first time we'll get this prompt saying to access our content we need to add the following URL as a course origin we just want to tell sanity the development server the port that we're running on and that it allows us to access all of our content on it we just need to hit continue and then add localhost 3000 as the core's origin from there you might need to log in one more time and once you do if you see something like this sanity is set up correctly and we should see that we have no document types we obviously haven't told sanity the content the shape of the content that we want to manage and we can do that through schemas before we get started with that however to make it a bit easier to head to our studio we can add a link to our header so if we head to site header and components at the very end of our header after our theme toggle which we have here we can add a conditional and say if we're in development if process.env.node EnV equals development then we want to return and show a link to Studio now why is this only in development well when this is pushed to production we don't want anyone else to be able to access the studio it's password protected as you just saw by having to log in through sanity however we don't want users to know about it so here we'll add a link from next.js where the href is set to slash Studio within it will add a button this is coming from our components slash UI folder we'll set the size to small the variant of the button will be ghost and in it will add an edit icon from the Lucid react icon Library where we'll set the classes H5 to give it a height of 5 or 20 pixels and a width of five and then when we save that if we were on our home page we could easily navigate to our studio just by clicking on these this edit button we also want to hide our header when we're in the studio because it prevents us from doing some important things it actually hides the publish button within sanity studio so to be able to remove or hide the component we can use a hook called use path name from next slash navigation to get the current path name that we're on and in this case it would be slash Studio slash desk we can write a conditional where if path name starts with slash Studio we want to return null and hide the component so if we were to save that now that's gone and finally we can move on to structuring our schema you can describe the different document types and in our case we're just going to have one and that is our product if we go to our sanity folder and the root of our project I've created this schemas folder and within it we have this product schema.ts file using this product object we're going to create our schema and Define all of the different properties or Fields as they're called in sanity that will be on each product these properties are of course defined by sanity and to get some help in figuring out whether we've configured our schema properly or not we can use the Define type helper from sanity to wrap our object our product is going to have first of all a name and we're going to call this lowercase p product we're going to give it a title of products capital P and plural this is what we're going to see in the sanity Studio and we're going to give it a type of document now all of its fields are going to go on an array and to get type information for all the individual fields we can use the Define field helper you can get a good idea of all of the different properties that we're going to have here by looking at the final version of our app we see that there's going to be an image which is in fact different images we're going to have sizes for our products a price a currency a description as well some additional metadata like the color and category our first field will be a name set to name title will be capitalized type will be string I'm not going to use Define field for each of these fields but feel free to do so the next one is going to be the name slug each product is going to have a unique slug that we're going to use for fetching products individually title will be Capital capitalized type will be Slug and using this options object we can set the source of the slug to name this will allow us to easily slugify it to create a slug out of the name within the studio user interface and after that we'll have the name images for all of our for our image gallery with the title images with type array and of to Define each of the array elements of Type image make sure you include this object within an array next we'll have name categories for our categories field with the title capitalized type array but this time of type string and we're going to copy this we're going to copy categories and paste it in two more times so we're going to use it to make our sizes field and our colors field because they are going to have the same type and we're going to have a description as we mentioned title capitalized and as you would expect it's going to be of type string and we can copy this two more times because we're going to have a skew a unique identifier for each product which is yep going to be Pro BF type string and we're going to have a currency field which is capitalized and of type string and last one we're going to have a price field and this is going to be a type number now that we have that we can save it and to load it and to tell sanity about it we need to go to within the sanity folder go to schema.ts and we want to import the product object from the product schema file so that's going to come from at slash sanity schemas slash product schema and when we save both of those files if we go back to our studio we now see underneath content this products option we don't have any products just yet but if we were to click on the create button it would give us a really nice set of inputs to be able to add all of the content that we need here and this is all according to the different types data types that we specified we're not going to take the hard approach in creating products we're going to see how to seed our sanity data and all this data is going to come from a special inventory file within our config folder you'll see some types here and if you look at this inventory array this includes all of the data that we specified according to the types that we specified within our schema now to load all of this we're going to need to go to our project at sanity.io so not our local Studio page but to the official sanity website make sure you're logged in go to your project and we're going to need to go to API the API Tab and go to tokens we want to add an API token this is so we can seed our data so we can give it the name of see data we want to give it editor permissions and hit save we're going to be given this particular token which we can copy if we head to sanity and we go to the lib folder within it and go to client if we go to create client we can add a token right here as a string and this is going to enable us to seed all of the data included in our inventory and this is with the help of a special script you'll be able to find this in the lib folder in seed.ts you can see everything that's happening here we're using the sanity client to add all the products as well as seed all of the images as well so to get this working we can head to our index page we can just go to app folder and go to page and what's so great about next 13 is the ability to use react server components it allows us to write a really succinct Syntax for performing async operations on the server you can see right now we're just exporting a component from our page file to seed our data we just need to make our function our component async just like we would a normal JavaScript function and we can await and call our async function here seed sanity data we just need to import that and when we save and if we open our terminal provided we have the index page open let's navigate back to it if we look in our terminal see the log sanity data seeded and if we go back to our studio you'll now see we have all of our items here with all of the appropriate values and images if there's an error in seeding the data as you can see here there's just an error that says invalid image fortunately I've included all of the product images here within public in this products folder so here it looks like in particular you can go through these but it looks like the last one didn't see it properly so we don't have any images we can fix this by just uploading the image manually and once you've uploaded an image you can just hit this button in the top right hand corner hit select and choose the image that you just uploaded you can see that the slug wasn't generated as well if we hit the generate button you can see what I was talking about it automatically slugifies the name and inserts it here with that we can hit publish and we can head back to our page and remove the seed script and now navigating back to our home page we'll figure out how to use sanity to fetch all of our products and display them here not only on our home page but across all of our Pages using the created sanity client here within page we can await client dot fetch to perform a query and this query is going to return an array of products we want to fetch all of the products to be displayed on the home page so we're first going to add a return type of sanity product as an array in other words an array of Sanity products and two fetch we need to provide a query which we can write using the grok helper so we're going to be using the grok query language that sanity comes with and to fetch all of our products we can say asterisk and we can write a filter within a set of square brackets where we want to specify that the type the underscore type of all of our data should be equal to the string product so that's the underlying product type that we created in our schema and we can insert the return value in a variable called products and for right now let's just console log the products you can save page and keep in mind that because we're performing this query on the server we're going to need to open up not the browser console but we're going to need to open up our terminal you can see here since I'm already on the home page we see an array of products you can see all the different fields here you can see the internal fields that sanity includes such as type and updated at as well as created at and the product ID to be able to specify which fields we want back because we don't in every case want to grab all the fields that are on each product we can use a set of curly braces and we can specify all the different fields that we'd like separated by commas so we can grab the ID the created at field as well as the name skew all the images the currency the price the description and if we look at what we got back specifically for a slug you'll see that slug was returned to us as an object so we can write our own custom field we can name it what we like in our case we still want to call it slug we just need to add a set of double quotes and a colon to say we want slug that property to be property to be set to slug.current so when we save again and if we scroll down we see just the fields that we specified being returned to us so now let's actually populate our page we'll begin with the header up at the top the name and description are going to come from site config .name and the description from site config.description within this H1 we want to display how many products our current our users currently viewing we can just replace the zero with products dot length and we can say that many results but it might not be plural we might want it to be result if it's just one result so we can add within a set of curly braces a ternary where if products dot length is equal to one we want it to be singular but we can add an S to results if not and after displaying the number of results we want to add a number of different components that will allow us to sort our products so we'll have a component for doing that we're going to have a number of filters here underneath on the left hand side and we're also going to have a product grid we'll begin underneath the H1 we see here a comment with product the text product sort so we will bring in product sort we'll include that and in the final version of our app if no products are displayed say we've included a filter that returns no products we want to display this no products found message and to do that we're going to head down to this div underneath the H2 here and we're going to take this class name and we're going to use a helper function it's being imported as CN and this is going to allow us to conditionally apply a class we can copy this class that's being applied on a large break point where it's setting the number of grid columns to 4 which is what we're going to be using by default when we have products so if products.length is greater than zero can write a ternary and say apply this class otherwise to display this no products found message we want the left hand side all the filters here to take up one width and this message to take up three widths we can do that with on a large breakpoint with grid columns and insert our own custom value of one fr one fractional unit and then for the second column three fractional units to take up three times the width and finally we can add product filters where you have that comment and the product grid component where we have that comment underneath again we have the very basic appearance of these components but we don't have the functionality yet we'll get started by displaying all of our products within our product grid to do that we'll need to pass down all of our products data as props so we can take a reference to products and create a new prop on product grid of the same name products and we can jump into product grid you'll see here that we have this props interface on it we can specify the one prop that we're passing down on our props object and this interface is helpful to declare the type of each of our props within our components so this is going to be set to sanity product an array of Sanity products and we can from the props object destructure the products property off of that and to declare our types we just need to add a colon and then provide the name of our interface here which will be props and we can now say we have this conditional up at the top can say that if products.length is equal to zero we can display our no products found message but if we actually have some products we can replace both of these empty arrays with reference to products we want to take each product and first of all since we're mapping over it here I've already taken the trouble to create the basic structure of each of our grid elements it's going to consist of an image the name of the product the price and this is all going to be a clickable link to be able to navigate to its individual page since we're mapping over these links we're going to need to provide a unique key and that's going to come from product dot underscore ID the href the link should be set to slash products slash product dot slug if you take a look at the individual page it'll be on the products route and then slash the unique slug for each of the products then as for the image we're going to be using next image these images are going to be served once again from sanity and this enables us to using next image create an image source by using the URL for image helper again this is provided by sanity and pass in product dot images which is an array we want to grab the primary image which is the first element in that array and then as a method to URL for image say dot URL and we can set the alt of the product to product.name we want the width of these images to be fixed at 225 and the height to be 280 and if we save both these files just temporarily we should be able to see our images here but we can make the image loading experience a bit better by adding a shimmer effect this may or may not be visible you might have seen it briefly right there it kind of fade in effect while our image is in the process of being loaded to add that effect we just need to set placeholder to blur and we're going to use the blur data URL prop to using a template string to set that to data colon image Flash SVG plus XML semicolon base64 comma and then we can use the helper function 2 base 64 and pass to that function another function called Shimmer that takes a width and height of our image which is again 225 and 280. this looks pretty complex this is included in the next documentation as well as both of these helper functions fortunately now that we've written this we can just go ahead and copy this going forward but this gives us a really nice effect for not too much code and finally we can include the name in our H3 from product.name and the price we can format with the help of the format currency string Helper and this comes from our shopping cart Library use shopping cart for this function it takes an object where it will receive the currency which again comes from the product from the currency field and the value is going to come from product Dot price so let's save all this and now we've got a pretty good looking product grid and the next step will be to make it possible to sort According to some different fields so if we go to our deployed app you'll see we have the option of newest which is the default of How It's sorted by default all the items in this product grid and we can also sort according from low to high price you can see the lowest price item is the belt all the way up to the brook Sunglasses if we go from high to low obviously that's reversed so not only do we have to add these different options here in our select but we need to figure out how to filter our products or rather to order them this is included the select is included within the product sort component so let's head there we don't need to pass any props to it and you'll notice up above we have this sort options array right now when we click on our select we just see the text sort options we want to iterate over these different options and display them here within select content so we can remove that and we can say sort options.map where we can take each option and we can use the select item component this is all coming from our UI folder from components UI select where the key is the option name so each of these options have a name and a value we'll provide the value prop as well on select item set to option dot value and display the text between the opening and closing tags from option.name so we're displaying these different options now but when we click on it we have to figure out how are we going to reorder our products and this is a nested component it's a component within page how are we going to change this query up here are we going to use something like State well unfortunately we can't just bring in use State and use the traditional lifting State up pattern where we pass down a callback to then change the state in the parent component we have to have a little bit different mental model when working with react server components if you look at the final version of our app if you click on newest or any of these options you'll see that what we're changing in the URL is a specific query parameter you'll see if it's price if it's a price option that's being selected it's the price query parameter and it's being set to the value descending or ascending and there's another one called date this matches what we see as the value in our sort options here and what we want to do is that whenever this changes whenever we detect that an item is selected we want to change the url we want to navigate to the URL that we've specified here to do that we can on the select use the on value change callback and we can write an inline function where this gives us the value and we can use the use router Hook from next navigation and we can take that reference to router and use router.replace and replace the current URL with the one specified on value so if we save sort and we now try changing the select we see our URL change as well we can get access to those query parameters or search parameters as they're known in next.js by heading back to our page and we have direct access to them within the props of our server component so if we were to take a look at our props object we can console.log it for now and we'll refresh and take a look at our terminal we'll see that we have nothing on this params property we'll cover what params is later but on search params we have a key value pair that's included on an object so we can get the key price and the value to be ordered on which is descending so to piece this all together using the grok query language we can order our queries by first adding a vertical line and then using the order operator where between a set of curly braces we can specify on what field we want to order on we can say for example price and we can specify whether we want to be ASC ascending and ascending order or in descending order so if we were to write that we see price and descending order from greatest to least if we were to reverse that our items are reversed we have everything now that we need in order to dynamically change our query to change the products whenever our search parameters change we need to add the search params property to props and we know that two of those values are going to be date it may not be there so we want to say dates optional this could be undefined instead of type string and price also optional line of type string once again we can destructure search params make sure to say the props object is of type props or props interface and now we can add a custom order variable so we can remove all this and just insert because this is a tagged template literal we can insert our own Dynamic value that'll come from order we can say that we want to include the price order and the date order and what these are going to consist of are two more variables a price order which will just be an empty string for now and date order and we just have to conditionally just write some conditional logic to say if we have a price and this is going to come from search params.price then we want to write using the syntax that I showed you we want to order on the price field using the value from searchprams.price otherwise if that param isn't there we just want price order to be an empty string and the same applies for date order the only difference is that this is going to be from search prams dot date and we're going to be ordering on the created at field so let's save this and test this out we'll try sorting by newest if we took a look at the value of the created at field for each of these products we could confirm that this is the case I'll just trust that it is if we order the price from high to low it's ordered properly and it seems to be the same for low to high so we're able to dynamically change our query here and we can improve it a bit by using object destructuring and from search params just grabbing date and price so we can now remove all those references to search params and it becomes a lot shorter as I mentioned the default sort is by newest and we don't have any query parameter for that so we do want to provide a default value for date of descending to make sure that we don't get an error The Next Step will be creating our different filters here we see that we have a bunch of sections we have three sections which we know according to the fields that we created for our product that these sections should be categories sizes and colors we'll head to if we go back to our index page head to product filters you'll see above our component an array with all the data that we need including the name of these sections and the different options I've included those for you as you can see in the final product so we want to display all of these using the data in this filters array we're mapping over filters within the span we're just going to change section to section.name and within section dot options we're mapping over each of the labels we have a key provided here and we have a check box we just need to add the label text and that's going to come from option dot label if we were to save that we can see we have all the sections and different options here but when we click on the label the Box isn't checked the reason for this is that we need to add an ID to the check box so this is going to be set to a dynamic value of filter Dash section dot ID Dash and then we're going to include the index of the option so this we can grab this index from the second parameter of the map function that's going to be a unique value and we need to add the html4 prop on the label so whenever we click on these labels the check boxes checked but more importantly once again when we click on these we want to change the search params you'll see that if we were to click on the category of bags it changes the search param of Category 2 bags and this is going to be a little bit trickier because we want to be able to append multiple options so you'll see here that we want the bags that are one size we have two results here so not only is category added but the size param is added as well so to make this work we're going to need to add an on click to our checkbox so we'll add an inline function here we're going to get access to the event and we're going to use a new hook within product filters it's going to be called use search params we're going to get access to the search parameters in our URL and we're going to need this to be able to modify them So within our function we're going to say new URL search params and pass in search params reference that we created and we're gonna put the result in a new variable called params this is something that we can then edit based off of whether the checkbox has been checked or not we can detect that by saying event dot current Target dot dataset dot state is equal to checked this is just an internal value of our checkbox component if checked is true we want to say params.delete section dot ID we want to remove that entirely you can see that in the final version when we have something that's checked and we want to get rid of it we un we click it one more time and it's removed from the URL so we're not filtering on that anymore otherwise if it's not checked we want to add it with params.set where we need to pass in the name and value so the name is going to come from section.id and the value from option.value and the most important thing is that we need to say router so again we're going to use router get a reference to that router dot replace and then using a template string slash question mark params dot two string so let's save all this and when we attempt to change the category we see the category is then appended onto our URL with the value of bags now we're not doing any filtering just yet there's one thing that we need to tackle first and that is if we refresh the page we see the URL stays the same but our option isn't selected so we need to then add some code that's going to set the value of checked on our checkbox component and check and see if a given option should be displayed as checked because it is in fact in the URL so for this we can convert these search params to an array that we can easily work with by saying array Dot from search params dot entries and this is going to give us something which we'll call search values it's going to allow us to determine the value of checked by iterating over search values and using the sum function where it will return true if just one element in this array meets the condition that we Supply so we're going to add another set of parentheses and destructure the key and value for each of these elements and we want to see if the key of this particular option is equal to the section ID and the value is equal to option dot value so now even if we refresh the page we'll see that bags is selected or whatever category that we've chosen one other helpful thing is that because these different sections collapse in the final version you'll see here that if we have something checked we have just one option checked in a given section you see it displayed here what we have checked this is a lot makes it a lot easier for our users to see what items are being displayed what items they they filtered or searched for so to add that bit of data we can within the span here that's next to the name add a ternary and say search params.get we want to get the search param according to the section so you know sizes categories colors if that exists then we want to display it within a set of parentheses so again we'll use searchprams.get section ID otherwise we'll just display an empty string pretty simple but once again we haven't really done any filtering we've written all the code to be able to change the url and make our check boxes work appropriately but we need to go back to our page and change our query here we're going to add a few more search params one for color category and size what's different about this is that we're not going to be using order we're not ordering anything we're going to be filtering so we're going to construct a filter using a filter variable and we're first of all going to include our product filter so we're going to include this once again it's just a string that we insert and our filter is going to start with an asterisk and have our set of square brackets we'll insert our product filter and then we want to add all the other filters afterwards so we're going to have three more for now the color filter the category filter and the size filter so we can insert all of those once again we need to check and see if that search param exists we can go ahead and destructure color category and size from search params we'll write ternaries the first one for color or if color is there we want to write a string where we say and so this is how we append multiple filters we would just add and to chain them one after the other and say and and to check and see if a given item is in an array we can just say for example if we want to see if black was in the colors array we would insert it as a string using double quotes but since this is a dynamic value we'll use the dollar sign curly braces syntax to insert that and we'll use that same string for each of these for category the difference being we're finding category string in categories and size we're going to find size in sizes so with all of that added we can save page and now immediately you see that we're filtering according to belts we could do the same for bags and we can filter according to size as well as color now the final perhaps most helpful manner of searching or filtering for something is by using an actual text search you'll see that we have that up at the very top where we can provide actual terms to search for it to filter for in the case of this bag here we couldn't say khaki or tote or bag remember that this is in the site header component you'll see that we have a form element here and the input within it the user is going to be able to type into the form and hit enter and perform that query based off of what they typed so we'll add an on submit to the form it'll be connected to a function a local function on submit where we'll get access to the event and this will be a react synthetic event coming from an HTML form element we will say event.prevent default to prevent the page from reloading to grab what's typed into the search input we can use form data Constructor and pass in events.current Target where we get back form data and from form data we can use the get method to select according to the ID which is search so we then get access to the search query the text that was typed in and we can change the url once again using router from use router so we can say router dot replace forward slash question mark search is the name of the search query param set to search query the value that was typed in so we can search for tote and that changes the URL but now we just see all of our products before we get to the actual filtering you'll see that if we refresh the page once again our search input's not populated so how do we grab the query param and use it to populate our input once again we'll use the use search params hook we'll get our search params and to get our default search query we can say search params.get search and we can say using the double question mark or the knowledge coalescing operator you can say if that's not there then we'll just use an empty string and this is going to be passed as the default value to the input so we'll set default value to default search query and when we save that we now have our input populated here to see to allow the user to see what they searched for previously now heading back to page to our home page you can anticipate what we're going to need to do we can grab the search search param we'll include that on our props object we'll destructure search but the way to search to do a text search according to a given text value is with the string again and name match in double quotes the value search so name is what we're searching according to if we wanted to change that to description we could do that as well we want the name field to match what we typed into search and this is going to be for our search filter value if search is there if not we'll use an empty string and we'll make sure to append that to our filter string here so let's save and search for tote immediately you see the products displayed they match the text tote you can change that to Canvas to just see this one here perfect when we click on an individual product from our product grid we'll be taken to slash products slash and then the product slug if we had to app and take a look at the products folder you'll see that there is this slug folder wrapped in square brackets meaning it's a dynamic route and we have a page here with no content what we need to do is use the slug to fetch the given product to be able to display all this information that we see here and you see on the individual product page then we have our main image as well as all of the important info about our product including the name price description size as well as an add to cart button and we have an image gallery at the bottom so heading back to our product page we want to make our function async make it a async component and we want to await client dot Fetch and in this case we're returning a sanity product and for our query once again we'll use grok we know from the home page that we need to at least include a product filter so set type we'll include a filter where type is equal to product but we also want to add a condition and make sure that the slug and we know that that value is going to come from slug.current is equal to in double quotes the slug that we're receiving from parameters so remember that we had both a params property as well as a search params property well prams allows us to get access to the slug or whatever the dynamic path is if we have a dynamic route like this one so we can grab the params property from the props object we just need to declare it and params is going to have a value of slug which is going to be a string and so the value is going to come from params.slug and we can put the return value in a product variable and log it to see if this works we'll make sure to navigate to an actual product so this queries being performed properly but you'll see that we're getting a return to us within an array so we just want the first element so to be able to do that we can get the zeroth element by adding a set of square brackets again at the end of the query if we load that one more time we should just get an object for this query we want to get a number of properties the ID created at we're also going to have an ID a custom ID field set to the underlying ID we'll grab the name the SKU all the images again price currency as well as the description sizes categories colors and we're gonna again grab the slug it'll be set to slug.current we will bring in the components product gallery and product info you'll see that we have product gallery at the top and product info underneath we're getting an error right now for product Gallery so we'll comment that out for product info we're going to need to pass the product data down to it so we'll have a product prop we'll go into product info and we'll make sure to add that the props object it's going to be of type sanity product so we'll destructure product and props is going to be of type props again the info is going to be all of the stuff that we see here and it's going to make it possible to add the given item to the cart with this add to cart button we'll Begin by inserting the product name from product.name as well as the price once again we'll format the currency string where the value is product price and the currency is coming from product.currency the description in this div it's gonna be set to product.description for the size we're going to use a special helper function called get size name so it's going to convert the actual underlying size to to something that's readable and this is going to be set to product dot sizes and we're going to get the zeroth element we want to then display if we take a look at a product with different sizes we want to be able to display some different options that the user can select so they can select a small belt or a medium belt because not all products are just going to be one size we want to display that here with these different buttons from product.sizes and again for the displayed size we're going to use the get size name Helper and pass size which is what we're calling the each value that we're iterating over and we'll see in just a second how to actually change that value if we take a look at the result right now we don't have an image but we have all of the text content here and let's navigate back to the Belt where we have a bunch of different options we want to make it possible to toggle these choices and see the size the selected size change so we need to have some State here and in any component where we're using State we need to add the use client directive this is very important we can't use State on the server and to actually use some State we can bring in the use State Hook from react and we want the default value of our selected size that's again going to come from product.sizes and then the zeroth element we're going to call the state variable selected size and the setter set selected size set selected size so instead of referencing product that sizes and the zeroth element here we're going to now defer to whatever is in the selected size variable and we can make this button work by adding an on click and using an inline Arrow function set selected size to whatever the current size is and additionally we can change its appearance we can see that it's selected or not by changing the variant according to what's in state if the size that we're iterating over is equal to the selected size we want it to have a variant of default otherwise the variant of outline so that's going to give us a better View of what is actually selected in the last step here within product info is to actually make this add to cart button work so what should happen when we add something to the cart well we want to select the size pass that along when adding it to the cart with our data so we'll take that data then when we click add to cart we see the item quantity increment in our shopping cart up at the top and we also see that toast notification at the bottom we can do all of this within the add to cart function which needs to be connected to the add to cart button just with an on click So within add to cart we need to First create our item to add we're going to create an item variable where we're going to spread in all the product properties and we're going to add a special property called product data this is going to be an object where we're going to include any additional info that should be included when an item is added to our cart so what we want to know is what size the user selected and that's going to be set to selected size now to add an item to the cart is really very simple we just need to call the use shopping cart hook which is coming from the use shopping cart Library when we call that hook get back an object that gives us a method add item and of course we could simply call add item and pass item to it however if we think about it when we add an item to the cart a second time we don't want to add multiple items we want to increment its quantity so we first need to check and see if a given item is in the cart we'll create a variable called is in cart and to get all the data of the items that are in the cart from the use shopping cart hook we can grab the cart details object and front card details it will include a bunch of key value pairs where the keys are the product IDs so we can see if a given product is in the cart according to its product ID and we can very simply just say cart details Dot and we're using the question mark here because this might be undefined and then if there is a key matching product ID here then we can say it's in the cart and to coerce this to a Boolean we can add a double not operator so now we can detect whether something's in the cart if if it's in the cart then we just want to increment the item and for that we can use the increment item function and pass in item dot underscore ID or we can add the item if it's not there as I mentioned we want to display the cart count in the header and the site header but be aware that we're not displaying that at the moment we're just displaying the hard-coded value of zero so here too we need to use shopping cart and there is a value called cart count which gives us a number this can be destructured from that object and we can just insert it here where we previously had zero for our cart button and then finally the last step is to use the toast hook this is again with the help of the toaster component that we have used toast is going to give us an object we can call a function from it the name toast where we can specify on it all of the relevant information when we're adding something we want to display a notification to somebody such as the title and at the top we can include the item name and in parentheses we can include the size that they've chosen by again using git size name and pass in selected size we can add a description by simply saying product added to cart and one other cool thing that you can add is an action so this allows us to add a component and this component is going to allow users to very easily go right to their cart with the help of a link so we can include a next link that'll point to slash cart this will include a button with the variant of Link in the class to separate it from the text content of Gap X2 and white space no wrap to make sure that the text here doesn't wrap and it's going to have the text within a span of open cart and we're going to use an icon from Lucid react Arrow right with the classes height 5 and width 5. so with all that let's attempt to add an item to our cart we'll select medium click add to cart and we see here in the bottom right hand corner the name of our product the size we see a link to go to the cart page so everything's working as expected and now we can move on to adding our image gallery here to be able to toggle between multiple images to preview the product before a user buys again this is going to use some State because we're making it possible to select one or another image we see that the main image here changes in response to the state so by default the first image the main image is being displayed and we can select any of these we see this little border around each of the images to see which one is selected to add this we'll go to page and uncomment product Gallery this does need the product data passed down to it so we will pass that down and add it to the props interface of type sanity product we'll destructure that prop and include our props type we'll make sure that our product Gallery component uses client up at the top because again we are using State and we're going to be working with an array here so to make sure that the first image is selected in our Gallery we'll use zero as the zeroth index and the name of this will be of this piece of state will be selected image in the setter will be set selected image and then we can head down to this empty array and change that with product dot images so we can map over each image and we want to grab the image index since we're iterating over this div we'll set the key of it to image Dot underscore key which isn't included this is another underlying property which isn't included on the type even though it exists so we're just going to say that it does exist with this as keyword to kind of overrule typescript here we're going to add an on click to this div where we're going to change the selected image by passing in the current index we have an image component here from next we can set the source with URL for image and pass in the image and call the URL method at the end of it so we get that back as a string we want to add a width of 200 and a height of 200 and keep in mind this is all for the image grid underneath the main image so it's going to be a bit small and then to include our Shimmer effect if you'd like to include that we can just head back to wherever we used it which was in our product grid we can copy those two lines and paste those in here and again the width is 200 height is 200. since there's the main image that we have down below we can paste that in as well and down here the source for our main image is going to be set to not image but product dot images and then from that array we can use the index which is again selected image and we can set the alt text to Main product dot name image where the width in this case is 600 the height is 750 and we'll add those to our Shimmer function and let's save page and product gallery and at this point let's go to an a product with multiple images we have our main image we have the ability to toggle between our images here when we click on them but we don't see what is the current image visually so to help our user out we can add this span which I've commented out and we just want to display it this this span if if I were to save it as is you can see it's just a border an outline for each of these images we only want to display it when the current image is the selected image so if we add a conditional and say if index is equal to selected image then in that case we want to add that outline at this point we now have our completed product page and we can move on to our cart page we want to display a summary of the items that our user has in their cart where it will also display a subtotal a shipping estimate the order total from there they can choose to check out or they can also change the quantity of each product that they like and you can see that we're changing the order total here and we're changing the number of items in our cart as well as remove items entirely where we have toast notifications showing a destructive action if we choose to delete them so let's head to our cart page again it's an app and in the cart folder we're just going to have two components here one for cart items and one for cart summary so let's add cart items and then cart summary we see right now we just have an empty left hand side of the screen that's where our cart items are going to be in our cart summary here is on the right hand side with all the information about the total cost as well as a button to check out which we see is just infinitely loading right now within cart items all of the cart data is stored on the cart details object again from use shopping cart if we were to log cart details we'd see what I was mentioning earlier about the fact that it's an object where each of the individual products unique products are added according to their ID as key value pairs we want to be able to display them to be able to map over them we need to convert this to an array we can do that easily with the help of object dot entries so we can turn that into an array of arrays here and we just want to grab the second element which is the product itself we don't really need the key so using cart details we can create a variable cart items where we'll use object.entries pass cart details to it and we want to map over this and again we need to add an exclamation point just to satisfy typescript we'll add a set of square brackets here to immediately destructure each of these elements and we want to skip the key so we'll just add an underscore here grab the product and we want to just return that so now we have an array of cart entries and using that array if we have no cart items if cart items dot length is equal to zero we can return a component cart items empty this is from our components folder to not display any content we would see no products added prompting users to add products to their cart that's just going to be a link but obviously if we do have items we want to take those cart Items Map over each product where the key is going to be product dot underscore ID we'll again create an image so we can copy all the familiar props here it's going to be 200 by 200. beneath that we're going to have a link that will include the product name so users can go back to the product even when they're in their cart page and it's going to go to slash products product.slug after that we want to display the price here where we have the text price using format currency string or value is product.price currency is product dot currency and here in the set of strong tags that we have we'll use get size name that function and pass in the size as you recall from the product data object so product.productdata dot size underneath that we have this label which tells us the quantity so we see quantity comma name we'll change name to product.name and for this input we want to set the Min value of it because this controls the quantity of a given item in our cart we want the Min value to be one the max value to be 10. we could set this to whatever we want we just don't want the minimum value to be zero or less than one that wouldn't really make any sense we can set the value to product.quantity this is a value that is stored internally if we were to take a look at the cart details and whenever we change the input we can add an on change and we want to get the event to get the the value the current value and we can use a new function from used shopping cart called set item quantity it's going to take the item according to its ID so we need to pass in product.underscore ID to that function and we need to give it a new quantity a new number value so we're going to need to use the number function and pass in event.target dot value and then finally we have our remove button where if we click on this we want to add an inline Arrow function that calls the remove cart item function and to it will pass our product now this is a local function we have here and within this function we're going to get access to the product and this is going to be of type product this product type comes from the use shopping cart Library from use shopping cart slash core so we want to bring in remove item from use shopping cart and pass in just the ID from product.id and once again we'll use a toast notification so we'll need to use toast we'll get back an object that gives us a toast property to call for this toast notification we'll have the title product.name removed with the description saying that the product was removed from the cart and the variant set to destructive is going to make our toast notification red to show that a destructive action took place and if we save cart items you can see our items here we can increment and decrement that down to one or up to 10. and we can remove it entirely if we're to refresh add one more item go to our cart and remove it we see that destructive action and we see our no content here our cart items empty component and then this order summary component is going to be pretty simple if we head to it we're once again going to use shopping cart up at the top where we want to grab formatted total price total price cart details and cart count we're going to first head down to subtotal where we see the text subtotal amount and we're going to change that to formatted total price we're going to assume if we're dealing with physical products that we're going to have a shipping estimate and the value that I'm going to use is just going to be five dollars this is going to be based off of whether we have any items in our cart so I'm going to make a variable called shipping amount and if cart count is greater than zero then we want to set that to 500. now this is incense it's got to be in the lowest denomination of your currency so when it comes to US Dollars that would be cents this is totally dependent on which currency that you've chosen and the shipping amount is zero if we have no items in our cart so for shipping amount we'll replace that with a call to format currency string and we'll pass in a value of shipping amount with the currency of US dollars we'll also calculate the total amount and that has to be the shipping amount plus the subtolum amount which is the formatted total price so we will calculate the total amount by taking the total price Plus the shipping amount so we get the raw value here and then we can format it with format currency string and we'll pass in the value of in this case total amount so we'll save cart summary and now we have no products in our cart but if we were to add one we'll add two we go to it we can see the updated order summary with the appropriate amount and we have a shipping value here if we remove one we see it decrement by the appropriate amount if we remove all of it that goes to zero the final step in building our app will be to enable users to check out the items that they've added to their cart will make this possible within the cart page if we head to the top of cart summary we'll see that there's an on checkout function which we want to call when we click on this button here at the very bottom now before checking out we want to add a couple of values one is a piece of state so we're going to make sure that we're using the use client directive We'll add a bit of loading state which will be initialized to false called is loading the setter set loading this will be true when we're in the process of creating the checkout the checkout is going to come from stripe we're going to see how to create that within the on checkout function but for right now we want to add a conditional around the loader icon and only display that if we're in if is loading is true if we're in the process of loading and additionally We'll add another ternary here to say if we're loading we want to display the text loading otherwise checkout on top of that we want to disable our checkout button here if we are loading or if we have no items in the cart so we'll create and is disabled variable where if is loading is true or if cart count is equal to zero we'll add then the disabled prop to our button and set that to is disabled as I mentioned we're going to be using stripe to create a custom checkout for us so you'll need to navigate to stripe.com create a free account if you don't have one already and once you've logged in you'll be redirected to the dashboard where you'll be given a developer publishable key and secret key we want to copy first the publishable key and we'll go to our EnV file and add that to next public stripe public key and for our secret key we'll copy that and add it to stripe secret key stripe checkout is going to give us a custom checkout page which we can configure if we just type in checkout in the stripe search and navigate to branding for checkout here we can choose the way that we want our store to look within the stripe checkout page so we can choose things such as an icon or a logo we can choose the brand and accent color and we can use a logo if we head to our public folder and take a look at the logo.png file and additionally we can add our brand color which we might want to set to Black and our accent color as white this also gives us the ability to customize things like the invoice payment page the invoice PDF Etc all of the different things that stripe offers to our customers when we've customized it the way we want we can hit save changes and with our environment variables we can restart our development server and the way that we're going to create our checkout is using the on checkout function but it's going to make use of some server side code that's within this API folder so we see that we have within app this API folder and within that one called checkout which contains this route file route is going to allow us to use next.js route handlers and specifically we're going to be using the post Handler to handle a post request that is made to this endpoint and the endpoint is structured again according to our folder structure so it's going to start with Slash API slash checkout and we'll need to make a post request to that so within post we're going to get access to request data that's of type request and we're going to send to this endpoint our cart details we're going to get that as Json data so we can await request dot Json and we're going to get our cart details what's important here is we can't really trust the client and what it sends if our data somehow has been tampered with and say somebody's figured out how to change the price that would be very bad so we need to figure out a way to validate the data that's been sent and reject this request if it's invalid if it's different than the official inventory and for this we can use the validate cart items helper function from use shopping cart slash utilities validate card items takes an inventory and this comes from that inventory file that we have in our config folder and it'll pass in cart details as the second argument this is going to return to us line items if this is valid and using stripe if we go to slash lib slash stripe this stripe.ts file you'll see that we've created a new stripe client with the stripe secret key we can only do this on the server we've specified a API version for our client and using that we can use the checkout API to say await stripe.checkout dot sessions we want to create a new session with the following values we want to set submit type to pay we want the mode to be payment the payments being made and we want the payment method types which is an array to include card we have a line items property which should be set to line items that we're getting back from our validate cart items function we can specify the shipping address collection this is an object where we need to provide the allowed countries that we ship to in my case I'm just going to choose the US you need to provide some shipping options since we do have an Associated shipping cost so we can head to shipping go to product slash shipping rates you see I have one created here we can create another shipping rate I mentioned that that was going to be five dollars for me and we could give it a description like ground shipping and we can include an estimated shipping time if we like if we save that that'll be created and we can just copy the ID that stripe returns and shipping options is an array so you can include multiple shipping options and we'll just include one object where the property shipping rate is equal to the string that we just copied we also need to include billing address collection this should be set to auto and finally we need to include the success URL as well as a cancel URL so the success URL is if the purchase goes through successfully the cancel URL is if the person exits the checkout page to send users to the right place we need to get the origin of the request which is just going to be in development localhost 3000 or in deployment it's going to be to our deployed URL we can get that from request dot headers .get and we can get the origin header to give us that value which we can use and a template string to say origin slash success to take users to the success page and we can append a session ID query param set to check out session ID just like this between curly braces and that's going to tell stripe to include the session ID of the created session in this URL that's going to allow us to get information about the purchase and the cancel URL should just be origin slash cart to take them back to the cart page so when we create a session we're going to get back session data we'll put that in a session variable and we just want to return using next response which we're importing from next slash server we can use the Json method to return that entire session now we can save our route and if we head back to cart summary we want to make a post request so we'll make this function async we want to set loading at the beginning we will await fetch and we'll make it the request to slash API slash checkout will include some options to set the method of our request to a post we'll set the body to json.stringify cart details so we'll send all that data to our endpoint we're going to get back a response from this request we will awaitresponse.json to get back all that Json data and we can use a new function called redirect to checkout from use shopping cart we can pass in data dot ID the ID of the created session this returns a promise so we need to await it and we're going to get back a result if there was an error and redirecting that's going to come from result dot error and we can just console dot error console.error the entire result and at the very end we will set loading back to its original value of false now we can attempt to navigate to our checkout page we'll make sure that we have some items added I'll add a bag and a belt we'll go to our cart We'll add two belts we'll hit checkout and we see that it's loading then we're taken to our custom checkout page we see that we're in test mode we see our two items as well as their quantity and a description feel free to remove this description property if you don't want to see this displayed here just delete it when you're adding an item to the cart and then we can add all of our information to pay with card we'll add our payment details and for card we just need to add 4 2 repeating in all of the fields and we can choose whether billing address is same as shipping we'll say that it is and we'll hit pay so we see that it's successful we're taken to the success page from the very last step before deployment is to complete this page to do so we can head to app find Our Success folder and using the search params prop can grab session ID we'll Begin by adding search params to our props interface so on search params We'll add session ID that may not be there so that'll be an optional string well destructure search params from props we'll grab the session ID if it exists from search params.session ID with the fallback value of an empty string and then using stripe we can await stripe dot checkout dot sessions and retrieve a session based off of its ID so just need to pass in the session ID there and we get back the checkout session of the payment that was just made and from checkout session we can get all of the customer related information from the customer details field so we'll put that in a customer details variable I can pass that down to another component called checkout session We'll add customer details as a prop and within this checkout session component this is going to be helpful for saying to the user that the order is successful to thank them and to tell them where they can find their invoice by reminding them about the email that they provided during checkout we'll grab customer details from props customer details is going to be of type stripe checkout session Dot customer details or null so we can copy all of that and just add it right here within the checkout session component if we don't have any customer details and there's no session ID search param we just want to say no checkout session found if we do have one we want to say thank you and insert the customer's name from customerdetails.name and we'll tell them to check their purchase email from customer details dot email and one last step is we would like to clear out the cart when they're done checking out so we'll use shopping cart and from that object that it returns will use the clear cart function we can call this within a use effect if we have customer details so we'll add that as a dependency and if we have customer details we want to call clear cart so now let's say checkout session and success page and perfect we see the order was successful thank you check your purchase email for your invoice and now we can either go back home or contact support if we need to actually be able to send an email an invoice on purchase stripe can do that automatically we just need to go to settings customer emails and then we want to email customers about successful payments and then hit save now that we've finished with our checkup process we're in a perfect position to deploy our app to the web for that we're going to be using versel you're going to need to have a free versel account so make sure you're signed into that as well as your GitHub account at github.com we'll go to github.com new to create a new Repository and we're going to set up a continuous integration pipeline by first pushing our code to GitHub and then deploying it with versel any future pushes to GitHub are then going to deploy our app via versel so we're going to add a repository name I'm going to call mine Ecommerce app we'll hit create repository I'm going to open up a new terminal and before I do anything there I'm going to go to my sanity client and remove my token the token that we created we don't need that anymore since we won't need to see data in the future I'm just going to remove a git folder if it exists with rm-rf.get and I'll run a clean get init git add period to add all the files I'll get commit deployment then you'll add your remote origin and then we'll run get push Dash U origin main after a few seconds should then be able to go to GitHub and see that all your code was pushed there successfully and then we'll head to versel and we'll add a new project we'll import the new repo that we just created and we'll leave the build and output settings at their default values and we'll go to our DOT EnV file copy all of these and paste it in to the environment variables section gives us this nice ability to populate all of our environment variables without having to manually input them and from there we just hit deploy and after a few minutes we see that our app has been deployed to versl we take a look at the final result we see everything's working like we would expect we can sort our items go to individual item pages change the size add them to the cart go to the cart and check out I hope you enjoyed building this eCommerce app and it taught you a bunch about next 13 feel free to use this in order to build your own amazing full stack stripe and next.js powered applications thanks again for watching and I'll see you in the next full stack project
Info
Channel: Build SaaS
Views: 43,072
Rating: undefined out of 5
Keywords:
Id: g2sE034SGjw
Channel Id: undefined
Length: 94min 35sec (5675 seconds)
Published: Tue May 30 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.