Built with Next.js 11.1.4 and Strapi 4.3.4. Code deployed on Vercel, HCMS uses PostgreSQL and deployed on Heroku, images stored on Cloudinary
Dependencies, 3rd party links and scripts
- guest shopping cart
- guest checkout
- no authentication (auth buttons just for show)
- checkout form data is saved between page reloads
- product search
- slider with magnifying glass
- currency change (3 available)
- discounts
- product reviews
- Google Analytics
- simple cookie banner
- pagination
- available item amount checks ("out-of-stock" and "less than selected") - here's the demo video
- PayPal checkout button (fake payments)
- email with order info is sent after fake transaction (it looks like this)
- React Context - example: 1, 2
- styled-components - examples: global style, ususal style, keyframes
- React Draft WYSIWYG (example) with draftjs-to-html (example) and html-to-draftjs (example)
- Formik - useFormik hook example: 1, 2; useFormik hook + Rich text (React Draft WYSIWYG) example: 1, 2, 3, 4 (credits goes to this codesanbox example π)
- yup - example
- React Responsive Carousel - example, with image magnifier
- react-image-magnifiers - example
- react-paginate - example
- SWR - example
- PayPal Checkout Button (react-paypal-js) - example: 1, 2, 3
- isomorphic-dompurify - example
- nodemailer - example: 1, 2
- Cookies banner - example: 1, 2
- Google Analytics - example: 1, 2
Purchasing process with guest cart and checkout
I divided it into 3 stages:
1. Product page
- When user navigates to page of product he wants to purchase, we:
- In addToCart component, we render "options" elements inside "select" element, based on "available" value
- When user chooses amount, we set this number to state
- Then, when user clicks on "Add to cart" button, we:
- pass selected amount, along with product id, to addToCart function
- create object out of them
- put object in localStorage
- set two states to show amount and Cancel button
- toggle cart badge state (that lives in _app.js) to show number of items in header, near cart icon: trigger assignItemsAmount() function to render badge
- If user'll change their mind, and click on Cancel button, we trigger cancelAdding function, where we filter out current product based on id, re-save cart list in localStorage and toggle all relevant states back
- Based on localStorage data, amount in cart for each item will appear in ProductListItem component and SearchResult component
2. Cart page
- Then, we have cart page to understand. Before we dive into cart, I need to explain render process and launching sequence of useEffects inside of it, depending on different circumstances. So: we have cart.js page, inside of which we have CartList.js component, inside of which we have mapped CartListItem.js components. The rendering process happens top-down, meaning: cart.js page -> CartList.js component -> CartListItem.js components, but the launching sequence of useEffects in all of those files is happening from down to top, like the event bubbling in JavaScript
- However, this is true only if we have all nested components already rendered (being already there). This will not be true, if those nested components are being conditionally rendered and are not initially there, but appear in process, during, for example, function launched in useEffect in parent page
- So, if user visits cart page for the first time: the state that toggles the render of nested components is initially "false", but turnes to "true" in function in useEffect of parent page/component. The whole sequence will be: cart.js page rendered -> useEffect of cart.js page is launched and triggered state that renders CartList.js child component -> CartList.js child component is rendered -> useEffect of CartList.js component is launched and triggered state that renders CartListItem.js child components -> CartListItem.js child components are rendered -> useEffects of CartListItem.js components are launched. If we present levels of nesting in ascending numbers, in this case useEffects sequence will be: 1 - 2 - 3
- If user visits cart page not in the first time and without reloading our website even once (meaning not resetting all the states in the app), then, that state, that allows to render nested component, initially will be "true". Then, the whole sequence will be: cart.js page rendered -> CartList.js child component is rendered -> CartListItem.js child components are rendered -> useEffects of CartListItem.js components are launched -> useEffect of CartList.js component is launched -> useEffect of cart.js page is launched. In nesting levels, presented in numbers, useEffects sequence will be: 3 - 2 - 1. I'm saying it so you kept this in mind to understand code better. I will reference this points below, when needed
- Now we can dive into cart. Whooosh! π When user navigates there (first time, see step #3), we launch assignProductAmountInCart() function, that lives in Custom App. Since we don't have authentication in this project (thus cannot bind amount to user in CMS), we store selected amount of products in localStorage. In order to create one guest cart with all necessary values, we need to fetch products from CMS, based on ids in localStorage, and loop through this fetched array of products, adding "selectedAmount" key-value inside each of them, from localStorage array. Inside of assignProductAmountInCart() function, we fetch the API route, attaching ids in query
- In api/cart route, we form a string of ids for Strapi's GraphQL query, fetch and send data back to frontend
- Back in assignProductAmountInCart() function, we manipulate the data as we need (I decided to sort in alphabetical order just because π€·ββοΈ) and set cart and cart length states
- The change of the latter state will render CartList component in cart.js page. The former will map cart items in CartList.js component
- In CartList.js component, useEffect launches initially and on "cartList" state change (in assignProductAmountInCart() function). Here we need to check if both localStorage cart list and fetched cart list are in sync, before (re-)rendering cart list. To do that we: 1.check their lengths to be the same, and at the same time 2.if all ids in localStorage are coincide with ids in potentially stale fetched cart list (first, we map boolean results of coincidences, then we check if any of them are false). Based on these two conditions, we launch 2 following functions
- First, we estimate total price of all items in function, that lives in Custom App. To do that we need to multiply price by amount of each item, and get the sum of the results of all those multiplications. This project doesn't have authentication, so we can't bind user's selected amount to public products right away. "price" value exists in one cart, "selectedAmount" in another. So, we create one common cart list from those two, and in the process, if ids from both cards are coincide, we put "price" value in the newly created cart. Then, we create an array with final prices, and if there is only 1 item, we set its price to state or, if there are more than 1, we set the sum of prices
- For the higher chance of convertion we show both total prices with and without discount. So, we do the same thing to create total price with discount (and set as a separate state). We check if there are any discounts in cart, and create the second total price with discount this time. The only difference in code is the check for the discount presence. Also, "areThereAnyDiscountsInCart" variable is passed through Context to CartList.js component to render prices and to show the message of how much money will be saved to make user feel more happy about himself ππ
- Second, we check the amount of items to trigger any errors. User may leave the cart and get back after a long time, the availability of products may change, that's why we need this check at the beginning of component's lifecycle on page load. We launch checkIfItemsAreAvailable() function in CartList.js (after estimateTotalPriceOfAllItems() in useEffect). Inside this function we pass both synced cart lists into helper function - checkItemsAmount(), that returns 2 entities: a boolean - whether any of items out of stock, and an array of ids of items - the selected amount of whose exceeded available amount in CMS (but they are not 0). For the first returned value we use ".some()" method to check if at least one of items is out of stock. It will return boolean value for us to trigger errors. We do not need a list of out-of-stock items, a boolean is enough. But for the second returned value (selected amount exceeded available amount) we need array of ids of items, because in cart we need to highlight "select" element (e.g. make its border red) in each cart item individually, to show user, that they need to reselect amount. So in this case we need array, because user has an option not to delete item from cart, but to reselect value, in which case we toggle the state of 1 individual item. So, here we use ".filter()" method on cart list from CMS, and for each of its items, we run a "for of" loop on cart list from localStorage. We check all conditions (if ids are coincide AND if available value > 0 AND if selected amount > available amount), if passed - the item is returned into shallow array copy created by ".filter()". And then, we make a new array of ids of returned items with ".map()" method
- Back in checkIfItemsAreAvailable() function we set the second returned value (array) to state. This state is passed down to child component(s) (CartListItem.js) as a dependency for useEffect, that runs a function, changing border colour of select element indivilually, as mentioned above. However, if function triggers state that is a dependency of useEffect, that runs another function, we need to rememeber that this another function will run after the end of first function, that triggered that state. It'll wait for that first function to finish its execution, and then runs itself (here it's function, changing border colour). So, we set the second returned value to state, then toggle two other boolean states, if either one of checks is true. The first state shows/hides error, the second disables/enables "Go to checkout" button
- Now, after checkIfItemsAreAvailable() function finished execution, as a side-effect of setting one of the states - in CartListItem.js child component(s) the useEffect runs toggleBorderColour() function. It does a standard check of the length of array of items' ids with exceeded amount. Then, if id of current item coincides with any of ids in there, it toggles border colour of select element in current item component
- Last function that launches at the start of page lifecycle (if page is loaded for the first time - see step #3) is "estimatePrice()" in cart item component(s) in useEffect. It also launches after "available" value changes, that depends on "cartList" value change as well (since the former is inside of the latter). Inside of it we look for the id of current item, if found - get the selected amount of that item. If amount is not exceeded available, we select the corresponding option in select element and set total price of one item, along with border colour (that we pass to item's stylesheet). If amount is exceeded - set option to "unselected" and total price to 0, along with border colour again
- We're done with initial useEffects' launches in cart, now let's see what user is able to do here. Inside of each cart item, user can edit the selected amount. When they do that, "editAmount()" function launches. In it, we:
- get the selected value
- create new cart list with that edited amount and put it into localStorage
- update item price and total price of all items (see step #10)
- re-check items availability to trigger toggleBorderColour() function again (see step #14) to toggle border colour (from red to lightgrey, in case when user edits "unselected" option)
- User can delete item by pressing the corresponding button. We launch deleteItem() function, inside of which we:
- filter out current item from localStorage
- if that was the last deleted item, we remove "cartList" value from localStorage, as well as "order" and "isFormSubmitted" - these last two are created during checkout process itself, we have yet to get there, don't bother with them now
- if that wasn't the last deleted item, we put filtered cart list in localStorage
- in any case, after delete operation, we have to re-run/re-set all the necessary functions/hooks again and those would be: assignProductAmountInCart() func (see step #5), setCartBadgeToggle() hook (see "Product page", step #4, substep #5) and estimateTotalPriceOfAllItems() func (see step #10)
- User can clear the whole cart with one button click. We launch clearCart() function, inside of which we do pretty much the same stuff as in step #17, substeps #2 and 4
- Now we can go to checkout page, by clicking "Proceed to checkout" button and launching goToCheckout() function. But before redirecting user there, we need to check if something is changed with items availability (what if user leaves website without closing tab or browser? Somebody may purchase some of those items before him, or browser may show cached data without refreshing the page, which user may not bother to do manually when he gets back. We have to do all checks on click as well). To do this we need data from cart, but I decided it would be too crazy to re-fetch the whole freaking cart with tons of key-values that we don't need π΅ Instead, we gonna fetch only ids and amounts from the custom-created API route (we're making this internal check more cheaper for traffic than it might turn out to be). So, right after clicking, we:
- disable the button
- get all item ids from localStorage cart
- attach them to query and hit the API endpoint
- there, we form the query string as Strapi wants it, fetch and send the response back (with only ids and available amounts)
- derived data we put into same helper function that we used in step #12 to check amounts by comparing cart lists (it'll accept cart list or data with only ids and amounts in the same parameter just fine π)
- set state that may or may not toggle border colour
- if at least one check hasn't been passed - relaunch assignProductAmountInCart() function (see step #5) that will trigger all occurring errors
- if passed - redirect to checkout page (we see that in this more frequent case expences are cheaper than if we'd fetch the whole cart)
- If user visits cart page not for the first time without reloading our website, useEffects will launch in a reverse sequence (see step #4). It may lead to some errors (such as undefined values, because some functions run before the functions they depend on). That's why here we're checking if carts are in sync, and when we estimate price for each item, we check if current item exist or not
3. Checkout page
- First thing we need to do on checkout page is to check if user is being a smart aleck and managed to visit this page without actually putting anything in his cart π In the first useEffect we launch redirect() function, that checks if "itemsAmountInCart" was set and equals 0. If it is - redirect back to cart, where user'll see "empty cart" message
- The checkout page has 3 child components: Form.js (form for customer's data), CartInfo.js (mini-version of cart) and PayPalCheckoutButton.js (payment button)
- Keep in mind that here we have multiple useEffects in one page, and functions in one useEffect will trigger state that is a dependency of other useEffects. The execution sequence will be: 1. the launches of all functions in all useEffects on page load, in order they were written (by "they" I mean all functions in each useEffect, and all useEffects in page), 2. the launches of functions in useEffects, that have a states in their dependencies, that were triggered by any of functions that were launched initially on page load. But further I will be explaining functions in the order that'll make more sense to understand code behaviour, okay? π
- Unlike in cart page, here we don't edit cart, it's purely informational. So, some checking functions that run here are similar to functions in cart, but more lightweight
- In the second useEffect we launch same old assignProductAmountInCart() function, that will set "cartList" state, that will trigger third useEffect, where we launch 2 functions: 1. familiar to us estimateTotalPriceOfAllItems() function, that sets total prices, and 2. setCartListInCheckout() function, that creates one cart list out of 2 and sets mini-version of cart list in checkout page
- Setting the state in setCartListInCheckout() function will trigger fourth useEffect, where we launch toggleErrors() function. Inside, it passes "checkoutCartList" value to checkItemsAmount() helper function, that based on single passed cart list (with all fields, necessary for a check) return 2 values: boolean - are any of items out of stock, and array of items, where selected amount exceeded available. Based on returned values, checkItemsAmount() toggles error message and checkout button state. Everything is similar to functions in cart, but slighty different, because the cart list in checkout is also different
- We have several functions in useEffects left to understand, but to do it better, let's analyze Form.js component first. When user goes to checkout page, he'll see the form powered with Formik. The default value of all inputs (except radio buttons, that are slightly different to handle) is set to Formik values, that are initialized with useFormik hook
- Despite that we can handle form data solely with localStorage, I decided to use both localStorage and Formik for the sake of learning. Every time user changes input values, we run handleFormFields() function. Inside, we need to put entered value in localStorage (to save on reload), and save it in Formik values. We declare name and value of current input. It it's a first time user enters value - create order item in localStorage and put key-value pair it it, based on declared inputs. If not - handle the case of deleting data from localStorage or put data in it as usual. As for Formik hook - all values saved with .handleChange() Formik function. Except radio buttons values, the example to whose I couldn't find in docs, nor in the Web, so I set it manually. When user changes delivery option, we change tax charge. We launch estimateShippingCost() function (that also initially runs in the second useEffect! βοΈ), that sets shipping cost state
- When user submits form, it is handled by Formik onSubmit function, where we toggle the visual state of form, by setting value in localStorage (to save it on page reload) and by setting state to "true". Also, we scroll to the top of form to make it look pretty β¨
- Visual view of form depends on that state, when it's true - it shows info with Formik values. By pressing "edit" button, the function of which sets values back, user can get back to editing whenever he needs
- Speaking of changing form's visual view - let's briefly get back to second useEffect, where we're launching setFormVisualView() on page load. It checks those values in localStorage and sets the state of form based on them
- Setting "isFormSubmitted" state will trigger the fifth useEffect, where we launch insertSavedDataInForm() function, that is needed for saving user entered data in form on page reload. Inside of it, after checking if "order" item exists in localStorage, we set all Formik values to the corresponding values from that item, it they exist. If they don't - set Formik values to themselves (doing that, we cover the case when form's state is "not submitted" and when some inputs weren't touched yet (hence - no values in localStorage)). It works for both form states and all that in just one line. Easy! π€ Except for those pesky dropdown (country) and radio buttons (delivery) - they need "special treatment" by getting and setting their values from DOM when form is not submitted (that's why we need "isFormSubmitted" state as a dependency in the fifth useEffect - to check its state). After we set delivery, let's not forget to launch estimateShippingCost() function, because shipping cost depends on delivery
- During insertSavedDataInForm() function run we set "formik.values.country" value, which will trigger the last useEffect, specifically setTaxAndFormBasedOnCountry() function in it. Inside, we get the country input's element and, depending on its value, set all necessary states: tax (goes to CartInfo.js), territory type and post code regEx pattern (both go to Form.js)
- Nothing much going on in CartInfo.js component - we just import all passed values and render JSX with them. Respectively, we map cart items
- Each checkout cart item should have different state based on available item amount. To cover "out of stock" case, we abstract DRY markup into variable and if available amount is 0 - render strikethrough element with "out of stock" message, if not - render usual "p" tag. To cover "selected amount exceeded available" case, we set amount and price to 0, and change text colour
- Our final component is PayPal Checkout button. We gonna use "react-paypal-js" library. To initialize script we need Client ID of app, created in PayPal Developer account (Dashboard -> My Apps & Credentials). To test fake payments we also need to create 2 sandbox test accounts (Sanbdox -> Accounts) - one "Business" (merchant) and one "Personal" (customer) type. In options of PayPalScriptProvider we specify environment variables of client-id and merchant-id π The latter we can find in Business test account info (hover over "..." button near sandbox account -> View/edit account -> Profile tab -> Account ID). Full reference to all provider options is here
- PayPalScriptProvider wraps our PayPalCheckoutButton.js component, where we initialize usePayPalScriptReducer hook and put PayPal code in useEffect to render button. Official example is here
- To show checkout button, we import PayPalButtons component. We need button to re-initialize whenever user changes total price or currency, so we put those values in "forceReRender" prop
- On checkout button click PayPal runs createOrder() function. We don't need to disable checkout button right away, because PayPal shows overlay loader. Before we run code that sets up payment, we have to check items availability one last time: get ids from localStorage, fetch items amounts based on them, create one common cart with all values and pass it to helper function to return check results. If not passed - launch assignProductAmountInCart(), that'll trigger errors. If passed - set up the transaction, as it shown in official example. We pass currency and total price, and set "NO_SHIPPING" preference, to show user the PayPal form without shipping fields (in case he'll choose credit card payment), that he already filled in our custom form
- If you want to see how the amount errors will trigger on all pages, you need to set up Strapi on your own machine to toggle amounts in CMS manually. But if you think it's not worth to set up, I made a video
- After PayPal transaction is finished, it launches onApprove() function, where we do stuff for our store. We disable checkout button and write PayPal promise (like in the official example), that returns data with generated order id. Now we just need to create order object to send it to CMS with all data in it, specifically: order id, time of purchase and payment method, that are coming from PayPal; customer info, that we store in Formik; purchased items, that we map from checkout cart list; and other values that we pass to button as props. This object we put in query and hit our API route - api/order
- In there, we need to do 3 things: post order data in CMS, update amount of items and send email to customer with all information.
- Before posting order, let's create a copy of "purchasedItems" array without "available" keys - we don't need this field in "purchaseditems" Strapi's component in orders collection (but we need it for subtraction further). Then, create a string of it (just like the string of object with ids before) for GraphQL query for Strapi
- We gonna do everything in async order:
- First, post the data with the corresponding query, which will depend on how you structured your collections in CMS. After posting, in .then() statement we create array with items with subtracted amounts
- At the time of writing, Strapi doesn't support bulk entries creation, but this feature is a candidate (if you use Strapi and would like to see this feature, please, upvote this π) So, for now we have to do multiple requests with PromiseAll. In query of each fetch() function we set the subtracted amount of the item
- Next, in PromiseAll .then() statement, we are using nodemailer to send an email to a submitted email address. First, create transporter, where we specify nesessary options, depending on email provider (I used Outlook). Api pages won't go into final code bundle, but for the extra layer of security, use env vars for your sender email and password π Also, turn off "rejectUnauthorized" flag for self-signed sertificate. Just in case of error, we'll fire the console logs (the .verify() method doesn't check if email was sent or not - that is up to email provider you chose, it verifies if Simple Mail Transfer Protocol is ready to work). Finally, let's send an email. Nodemailer code is based on the official examples: here, here and here. In Google Mail the email will look like this
- send the response with order id to the frontend
- Almost finished! Back to PayPalCheckoutButton.js, in .then() statement, after everything we've done, we delete all data: cartlist, order and isFormSubmitted items; trigger cart badge to disappear; and redirect user to "thank you" page with order id
- And in "thank you" page we show a success message with order id. If no id was provided in query for any reason, we redirect user to the main page
Currency change
- User can change currency in Footer's SelectButton.js component, in select element
- On select change we launch changeCurrency() function, where we put the grabbed value into localStorage and launch refreshCurrency() function, that lives in _app.js
- In there, we set "isCurrencySet" state to false to show loader where we need, set "currency" state to a value saved in localStorage and set "isCurrencySet" state back to true to show currency instead of loader
- Setting "currency" state will trigger second useEffect in _app.js, that'll launch setCurrencyRates() function. Inside of it, we use switch operator to set "currencyRate" and "currencyCode" states based on "currency" state
- We set "currencyRate" state to value from fetched data from 3rd party currency API (I used openexchangerates.org). API has monthly limit, so in case it will be exceeded, just for the pet project to work without crashes, we set fallback value
- To save user's currency choice, we launch setCurrencyCodes() function in the first useEffect on page load. Inside it, we set state to show loader and check if "currency" item is present in localStorage. If it isn't, meaning user hasn't selected currency option before - set it to default, if it is - hit custom "currencyRates" API route. In there we fetch the exchange rates from 3rd party API and send derived data in response. Back in _app.js, we set response into React state and, like on user click, as we described in steps #2-5 - launch refreshCurrency() function, that will trigger setCurrencyRates() function in the second useEffect, that'll do the magic β¨
- Also, in Footer's SelectButton.js component, on page load we visually set one of select options based on item's value in localStorage
- Below, in "Info about where Context data goes" section you can see where all states, connected with currency, go in this app
Product reviews (Rich text editor + Formik)
- On product page we have form with 2 usual HTML inputs (name and email) and React Draft WYSIWYG rich text editor
- To grab the values we need to set them to their default state with useFormik hook first. For HTML inputs we set "value" attribute to Formik value. Every time when user enters name or email, we launch "formik.handleChange" function that puts them into Formik values
- For RTE it's different. We have "editorState" prop, wich will be set to React state. In this state we check if formik "message" value is empty (value, that we declared earlier): if it's empty (empty string === false) - set state to initialized empty RTE state, if it's not - set it to prepareDraft() custom function, that accepts Formik "message" value as a parameter
- Every time when user enters text in RTE, we launch onEditorStateChange() function, that we put into RTE prop. Inside it, we:
- convert entered text into HTML using "draftjs-to-html" library
- put derived HTML into another function that will explicitly set it to Formik "message" value with ".setFieldValue()" method
- trigger the React state change, that will launch prepareDraft() function we mentioned in the previous step
- prepareDraft() function takes Formik "message" value. Then:
- Formik "message" value is converted into DraftJS Editor content, using "html-to-draftjs" library
- DraftJS Editor content is converted into ContentState record, using createFromBlockArray() function
- ContentState record is converted into EditorState object, using createWithContent() function
- derived EditorState object is returned to "editorState" React state
- All credits goes to this codesandbox example π
- After user submits review, we launch Formik's onSubmit function. Inside, we set the state that will disable both editor and submit button. Then, we fetch postReview API route with product id and submitted data in query
- In api/postReview, before posting, we sanitize data with "isomorphic-dompurify' library. Then, after we send the request, we must ensure that collection has one review per person. We do that by checking if submitted email is already present. If it is - Strapi will attach "errors" object and in this case we send status code 400 (bad request) with custom error message, if it isn't - send status code 200 with response data (it'd be posted review)
- Back in product page, we check response on status code, if it's 400 - we set error message to React state, that we declared earlier, and throw err object; if it's any other 40X - we do the same, but set custom error state to false. This state will toggle error message
- If no errors pop up, in the next .then() statement we reset Formik form and RTE state, and set error to false. Reviews are rendered from "reviews" state, so we push response data (posted review) and refresh the state. In the end, we disable editor's "read only" and scroll to the top of review list to let user see his review
- On page reload, based on passed id from getServerSideProps() context object, we fetch data from 2 endpoints: "product" and "reviews". If you don't use Strapi, you're probably creating your own API, so, further info won't be of interest to you. You can stop reading now π If you do use Strapi, I have some points left to say at the end
- At the time of writing, Strapi doesn't support array field type. It would be easier to handle product reviews with it. So far, it is only a feature request (if you would like to see this feature, please, upvote this π)
- We could create "repeatable component" field type, but AFAIK, in this case, for every new submitted review you have to re-replace ALL components (other reviews, that are already in CMS) in one shot (same case with "custom JSON" field type), sending them ALL to CMS along with the new one. If we have hundreds of reviews, this is sucks! For traffic and performance
- The only option I see is to create "relation" field type: one product has many reviews. Creating "reviews" collection and relating it to "products" will allow us to post one entry without harassing others. That's why, when user visits products page, we fetch data in 2 requests: product data and reviews
Show/hide
areCookiesAccepted
goes to:
- components/Layout.js
setAreCookiesAccepted
goes to:
- components/Layout.js
- components/layout/CookieBanner.js (as props from components/Layout.js)
cartBadgeToggle
goes to:
- components/layout/header/CartButton.js
- components/productPage/AddToCart.js
- components/cart/CartList.js
- components/cart/cartList/CartListItem.js (as props from components/cart/CartList.js)
- components/checkout/PayPalCheckoutButton.js
setCartBadgeToggle
goes to:
- components/productPage/AddToCart.js
- components/cart/CartList.js
- components/cart/cartList/CartListItem.js (as props from components/cart/CartList.js)
- components/checkout/PayPalCheckoutButton.js
itemsAmountInCart
goes to:
- pages/cart.js
- pages/checkout.js
- pages/checkout/CartInfo.js (as props from pages/checkout.js)
cartList
goes to:
- components/cart/CartList.js
- pages/checkout.js
setCartList
goes to:
- components/checkout/PayPalCheckoutButton.js
totalPriceInCart
, totalDiscountedPriceInCart
and areThereAnyDiscountsInCart
go to:
- components/cart/CartList.js
- pages/checkout.js
- pages/checkout/CartInfo.js (as props from pages/checkout.js)
assignProductAmountInCart
goes to:
- pages/cart.js
- components/cart/CartList.js (as props from pages/cart.js)
- components/cart/cartList/CartListItem.js(as props from components/cart/CartList.js)
- pages/checkout.js
- components/checkout/PayPalCheckoutButton.js (as props from pages/checkout.js)
estimateTotalPrice
goes to:
- components/cart/CartList.js
- components/cart/cartList/CartListItem.js (as props from components/cart/CartList.js)
- pages/checkout.js
currency
goes to:
- components/ProductListItem.js
- components/SearchResult.js
- components/product/productPage/ProductInfo.js
- components/cart/CartList.js
- components/cart/cartList/CartListItem.js (as props from components/cart/CartList.js)
- pages/checkout.js
- pages/checkout/CartInfo.js (as props from pages/checkout.js)
- pages/checkout/cartInfo/CheckoutCartListItem.js (as props from pages/checkout/CartInfo.js)
- pages/checkout/PayPalCheckoutButton.js (as props from pages/checkout.js)
currencyCode
goes to:
- pages/checkout.js
- pages/checkout/PayPalCheckoutButton.js (as props from pages/checkout.js)
currencyRate
goes to:
- components/ProductListItem.js
- components/SearchResult.js
- components/product/productPage/ProductInfo.js
- components/cart/CartList.js
- pages/checkout.js
- pages/checkout/CartInfo.js (as props from pages/checkout.js)
- pages/checkout/cartInfo/CheckoutCartListItem.js (as props from pages/checkout/CartInfo.js)
- pages/checkout/PayPalCheckoutButton.js (as props from pages/checkout.js)
isCurrencySet
goes to:
- components/ProductListItem.js
- components/SearchResult.js
- components/product/productPage/ProductInfo.js
- components/cart/CartList.js
- pages/checkout.js
- pages/checkout/CartInfo.js (as props from pages/checkout.js)
refreshCurrency
goes to:
- components/layout/footer/Buttons.js
setItemsAmountInCart
, setTotalPriceInCart
, setTotalDiscountedPriceInCart
, fetchedRates
, setFetchedRates
, setCurrency
, setCurrencyCode
, setCurrencyRate
, setIsCurrencySet
, setCurrencyCodes
and setCurrencyCodes
stay in pages/_app.js
- the loader has been taken from this codepen example π
- since it's a demo, all pages have meta tag with content="noindex" attribute
- html-to-draftjs library is deliberately downgraded to 1.4.0 to avoid bug (see issue #78)