Creating Products API0:05
Up until now, we've been loading a hardcoded product list in our home component. Let's change this to an API. Instead, for the API, I created alar our product. And let's take a quick look. First thing I did was create a migration for our products. We have an id, we have our title, we have our price, description, and bullion, whether or not to use the product for upselling, but more on that later.
or not to use the product for upselling, but more on that later. Do note that we use the decimal type for our price to prevent rounding errors from occurring in the database. Then I created a Product heater and I loaded up our original products and I added three more water, soda, and beer, which we will later use for upselling. If you run php artisan migrate:fresh, and then we run php artisan db:seed.
If you run php, artisan migrate:fresh, and then we run php artisan db:seed. We can dive into our database and we can see our products table has been created with our products prefilled, pretty nice. Then I created a route api/products, and if you dive into the ProductsController, we'll see we load every product and we return a collection of ProductResource. If we visit the page in our browser, we'll see a list.
Setting Up Products Store1:08
and we return a collection of product resources. If we visit the page in our browser, we'll see a list of all products being returned. Now that we have an API, let's jump back into our view project and create a new store. Let's call this products.js. In here we can define our store. We'll say import defineStore from pinia. And remember our convention. We will export a constant useProductsStore.
Define store from pia. And remember our convention. We will export a constant use productsStore and we'll define the storeProducts. And this time let's create an option store. Let's define or state we can do so as follows, we'll say products and let's initialize it to an empty array. For now, let's jump to our homepage. And actually let's just copy this entire definition. Let's remove this.
And actually let's just copy this entire definition. Let's remove this. Let's jump back into our store and let's paste it here. Let's jump back into our home component and let's make sure our app still works because product is now not defined anymore. We can resolve this by importing use products store. Let's initialize the store. We'll say const productsStore equals useProductsStore.
Store. Let's initialize the store. We'll say const productsStore equals USProductsStore. And instead of looping over products, let's loop over productsStore.products. And if you refresh our application, it still works Suite. We can open up the dev tools, we can scroll down to Pina, and now we'll see that next to the card store. We also have the productsStore with our predefined products in it. Now instead of hard coding
Fetching Products from API2:35
with our predefined products in it. Now instead of hard coding or products, let's fetch them from our API. The first thing we'll do is remove our hardcoded products. There we go. Next, let's add an action. We can do so under the action key and let's call this Action FetchProducts, and it'll be an asynchronous action in here. We'll do our API call, and we can do this any way we like.
We'll do our API call, and we can do this any way we like. As long as it's promise based, you could use Fetch, but I find it a bit verbose, so I'll use Monday. Monday is a very simple wrapper around fetch. So let's first add it to our product, then let's import it. And the way Monday works is that we have to create a small layer for products API we can do so as follows. const products = mandate,
for products API we can do so as follows. Cons, products equals mandate, and let's pause in our URL for our products API. Finally, in our action, we can do our call. Let's initialize an empty data array. Let's try to do our API call. We can do so by saying, oh wait, products cat. And if it fails, let's catch and log the error to our console. Our response contains a data key.
and log the error to our console. Our response contains a data key. So let's say data equals response of data. And finally, let's say this products equals data. Now when we go to our homepage, the only thing for us left to do is to call productStore.fetchProducts. And when we visit our product, we can see our products being loaded from the API and we can validate this because we now have our water or soda and or beer.
Adding Loading Indicator4:09
and we can validate this because we now have our water or soda and or beer. But we can also head to the Network Inspector Refresh and we'll see our API call. Now when we refresh the page, we'll see our products being loaded almost instantly, and that's because we're developing locally. Let's introduce some artificial delay in our Laravel backend and see how our app behaves in our ProductController before responding.
and see how our app behaves in our ProductController before responding. Let's sleep for one second. Now when we refresh the page, we'll be greeted with a blank page before displaying the products, which isn't that nice. Let's improve this behavior and add a loading indicator. Let's open up our product store and let's keep track of the loading state. In our state, we'll define a property called loading.
and let's keep track of the loading state. In our state, we'll define a property called loading. Let's initialize it to false. And in our fetchProduct action, we'll say this, loading equals true. And down below we'll say loading equals false. In our homepage, let's say we only render the grid. When we are not loading, we'll say if productStore.loading equals false and otherwise, let's render loading, let's say text-center.
If productStoreLoading equals false and otherwise, let's render loading, let's say text-center and let's give it some padding. And now when we visit the page, we'll see the loading indicator for a second. Before rendering the products, instead of rendering text, let's clean the zip some more and add an icon instead. Let's say ArrowPathIcon with a class of size-6. And let's animate it as follows. Don't forget to import it. Now when we refresh, we get the spinning
And let's animate it as follows. Don't forget to import it. Now when we refresh, we get the spinning icon, but let's center it. Let's give a reparative, a glass of flag items center, justify center, and maybe add some padding. This is looking much better. If you dive into our store, we'll see or get product action. There's two mutations directly after each other. We'll set the products and we'll set the loading state to false.
We'll set the products and we'll set the loading state to false. PIA offers us a way to apply multiple changes to our state at once using patch. So we can replace these two lines with this patch. We'll say products equals data, and we'll say loading equals false, and we can get rid of these two lines. Now we go to our checkout page and return. We see our products are being loaded.
Preventing Duplicate Fetches6:16
Now we go to our checkout page and return. We see our products are being loaded again, which isn't ideal. Let's modify or store once more and let's keep track of whether or not the products are already loaded. Let's add a boolean didLoad with an initial state of false, and after the products did load, we can say didLoad true. Finally, in the beginning of our action,
after the products did load, we can say didLoad is true. Finally, in the beginning of our action, let's check if the product's loaded. Let's check if didLoad is true. And if it's true, we simply return. Finally, let's add a way to override this force equals false. And in here, let's say force equals false. Now when we visit the homepage, our products will load. We can go to the checkout page and return, and our products are still loaded.
Building Upselling Product Grid6:58
We can go to the checkout page and return, and our products are still loaded. So we are not doing a round trip to the server anymore. Let's say we want to add a frequently bought together section to our checkout page so we can upsell some items to the customer. And let's say we want to reuse our Product grid of our homepage. Let's go to our homepage and let's create a ProductGrid component of our grid.
Let's go to our homepage and let's create a component of our grid. We'll call this ProductGrid. Let's create our component ProductGridView, and let's paste the grid we copied from our homepage. Now, instead of relying on our productStore, we will dumb this component down and we'll define two props. Instead, we'll say props equals defineProps. We'll define products of type Array and it's required, and we'll define loading of type Boolean.
We'll define products of type A and it's required, and we'll define loading of type bullion and it's required as well. And let's modify our template. So instead of reading product sort loading, we'll read it from our props and we'll do the same for products. Now, the reason we do this component down is actually quite simple. On our checkout page, we want to display different products,
down is actually quite simple. On our checkout page, we want to display different products, and that logic should live outside of the grid component. The grid component should only worry about rendering the list it got. So back in our homepage, let's say products equals productStore::loadProducts, and let's say loading equals productStore::loading. Finally, let's import our ProductGrid component, and let's check whether or not our homepage still works.
Finally, let's import our ProductGrid component, and let's check whether or not our homepage still works and it still works great. On our checkout page, let's say we want to add the grid above the confirmation button. So let's open checkout, let's find our button, and let's say ProductGrid. We say loading equals productStore.loading products equals productStore.products. Let's make sure to import our ProductGrid.
products equals productStore products. Let's make sure to import our productGrid and let's set up our productStore. First we'll import it, use productStore, and then we'll instantiate it. Now, when we visit the checkout page, we'll see our productGrid being rendered. Let's add some padding below. Let's see, class="mb-3". That looks a bit better. When we reload the page while we're on the checkout page,
Let's see, glass MB three. That looks a bit better. When we reload the page while we're on the checkout page, our grid is suddenly not rendering anymore. And this makes sense because remember, we are only loading our products on the homepage. So if we go to the homepage, we'll say products sold out, fetch products. And obviously when we don't pass the homepage or products are not being fetched by Pina, we can easily fix this by opening up our app view.
or products are not being fetched by Pina, we can easily fix this by opening up our app view. And instead of fetching the products in our homepage, we can fetch them in our app because we can safely assume that this data is relevant to more pages and components than only the homepage. And now when we refresh, while we're on the checkout page, we'll see our spinning indicator and our grid loads. Great. Now, instead of rendering every product, let's only render the products that have uh, use
Great. Now, instead of rendering every product, let's only render the products that have the useForUpselling flag set to true. And we can do so by creating a getter. Let's go to our ProductStore. Let's create an entry for our gutters, and let's call this upsellingProducts. And let's return our products. We can filter them, let's say product, product->useForUpselling.
We can filter them, let's say product, product use for upselling. Finally, in our checkout page, instead of passing in the product, let's say upselling products, and now when we refresh, we'll only see our water or soda and our beer, great. Now we can add some more logic to the upselling products to make the recommendations even more relevant, because now when we add water to our cart, we'll still get recommended water.
because now when we add water to our cart, we'll still get recommended water. And this doesn't really make sense from a customer perspective. So instead, let's also filter out the products that have already been added to the cart. Now in our upselling products gather, we have to find a way to check the cart contents. And remember, our cart state lives in our cart store. Wouldn't it be great if we could access the cart store
And remember, our card state lives in our cardStore. Wouldn't it be great if we could access the cardStore inside of another store looking for us? This is actually very easy to do in Pina. We can just say, const cardStore = useCardStore(). Don't forget to import it. And now inside our gather, we have access to our entire cardStore. So instead of only filtering on the use for upselling bullion,
So instead of only filtering on the use for upselling bullion, let's also check if the product has been added to the cart and let's say cartStore.amount for product. Let's pass in our product is equal to 0. The amount for product gather hasn't been created yet. So let's dive into our cartStore and let's say $amountForProduct equals gather with a parameter of product. And if you remember correctly,
It's gonna be a gather with a parameter of product. And if you remember correctly, we can actually dive into the product cart. And we already have this logic, so we already have our amount here. We can actually just cut this and paste it here, do some modifications so we can say carValue::find. And then instead of prop product, let's take our argument. And if we have a value, let's return the quantity and
And then instead of props.product, let's take our argument. And if we have a value, let's return the quantity and otherwise let's return zero. Now remember, this is a setup store, so we have to return together and in our product card, let's return cardStore.amount for product, and we'll say props.product. Let's first check that our product card is still working, so we'll still get our amount, and that's great. And now when we go to our checkout page,
so we'll still get our amount, and that's great. And now when we go to our checkout page, we'll see we get recommended a soda and a beer, but not a water because we already added the water to our cart. If you remove the water from our cart, we'll see it reappear in the grid below, and when we add the product to the cart, we'll see it disappear. And that's pretty great. And there we go.
we'll see it disappear. And that's pretty great. And there we go. Our upselling now works as intended. Great, and as a finishing touch, let's add an indication for the recommended product here recommended with your purchase. And let's make the text a bit nicer, text-algae font-bold, and maybe add some padding. And let's only render this when we have at least one upselling product.
And let's only render this when we have at least one upselling product. VF length, larger than zero. And there we go. Our upselling works great. Wow, we covered a lot in this lesson. We started by creating a new OptionStore in which we fetched our products from an API. We then updated our UI to reflect the loading state, and we added an upselling section to the checkout page. Because Pina makes it easy for us.
and we added an upselling section to the checkout page. Because Pina makes it easy for us to reference stores from within other stores, we were able to easily check if the product had already been added to the cart. And if it was, we were able to filter it out. That's it for this lesson. I'll see you in the next one.
