- E-commerce website built with React class components, react-router, and styled-components.
- It fetches data from a GraphQL endpoint with Apollo Client
Before start explaining the process and the code, I find it useful to understand the project structure first. There are five folders, assets, pages, components, store, and utils.
- assets: it has all the icons the project needs
- pages:
- It has the three required pages, category page, product page, and cart page.
- Every page has its own folder that contains the page component, its children components that are not used anywhere else, and their styles files.
- components:
- It has all the components that are used in more than one place(in two components for example), or they are simply not a page.
- Every component has its own folder that contains the component itself, its style file.
- store: it has all folders related to redux-store, e.g actions, and reducers.
- utils: contains the helper functions and the grapghQL queries file.
- Inside
index.jswrapped the<App />component with<ApolloProvider client={client}/>and provide it with the ApolloClient instance. - Created the different pages needed (with dummy content that indicates the page ), and the
Navbar. - Inside app.js setup routes with react-router and define each page path.
- I used
react-routerv5 because v6 is moving away from claas components and focus heavly on using hooks.
- Set up redux and create a
rootReducerthat will hold the app's different reducers - Created a
currencyReducer, which holds the selected currency symbol. - Defined a
GET_CURRENCIESthat will grab all the available currencies in GraphQL endpoint. - Created a CurrencySwitcher component, wrap it in the
graphql HOCto get access to the data (currencies we get back from the GraphQL endpoint) in the compoenent props. - Map the redux state to props, and map a
selectCurrecnyfunction, which dispatches an action to change the selected currency, to props as well. - Output the currencies in a dropdown menu and add an onClick event that will call the
selectCurrecnyfunction, thus changing the selected currency in the redux store. - Implemented functionality to close/open the dropdown menu, and close it when clicked outside of it.
- Created a CategorPage component, and wrap it with the
withRouter HOCto get access to the route parameters in the component's props. - Grab the name of the category from the route params, and pass it as props to the
<ProductsList />component.
- Defined a
GET_CATEGORY_PRODUCTSquery that accepts aninputvariable, then use that variable to filter the products by category name - Created a
<ProductsList />component and wrap it with the Apollographql HOC. - Aas a second argument for the HOC pass an object that defines an options key. The option's value is an arrow function that takes the component's props and returns an object.
- Inside that object define the query variables, and give the
inputvariable thecategoryNamethat was passed as props. - Iterate through the
products, which are filtered from the backend depending on the categoryName, I get back from the GraphQL endpoint. - Pass the prices (prices that correspond to different currencies) for a single product to the
<ProductPrice />component, that will output a single price.
- Created a
<ProductPrice />and map the redux state to the component's props, to know which currency is selected. - Created the
getAmounthelper function that takes the prices of a product and the currency symbol. it uses the.filterarray method to get the price that matches the current currency selected and return it.
- It follows the same pattern as the
<ProducList /> component, the only difference is it grabs a productidfrom the route params instead of a category name and it use thatidto get the specific product from the GraphQL endpoint. - It passes gallery as a prop to the
<Gallery />components, which output a gallery view. A main picture(the big one) and the rest are thumbnails. - When you click one of the thumbnails it changes the main picture to that thumbnail.
- It passes the product attributes, and the
getAttributesfunction to the<ProductAttributes />component.
- Created
<ProductAttributes />that output the differentt groups of controlled radio buttons (Size, Color..), Each radio button represents an attribute. - Added an onChange eventListener that fires the
handleSelectfunction. handleSelectwill call thegetAttributesfunctions, which will update the parent component (<ProductDescription />) state to be in the following format{GroupName: attribute}(example: {Size: 'M'}).- Styled the selected attribute differently depending on the checked attribute.
- If a an attribute has a swatch attribute are iutpteded as colored squares.
- After I get back the data from the GraphQL, the Apollo Client stores that data as a flat object in a local, normalized, in-memory cache. So, the next time you ask for the same data it will grab it from the cached data.
- However, I had a problem due to how Apollo generates cache ID to identify each object and because the attributesSets in the QraphQL schema have the same id, even for different products ( AttributeSet Size for example has the id
Sizefor all products).
By default, an object's cache ID is the concatenation of the object's __typename and id (or _id) fields, separated by a colon (:).
- The problem is the attributes will be messed up and all the products will have the same attributes for each group.
- As a solution for this, I needed to customize the generated cache ID to use the array of items
[items]to be the unique identifies instead of the attributeSet id.
- Create a
cartReducerand add it to therootReducerascartItems. - Define the action creator
addProductthat takes an object (the product added) as an argument and returns an action with thatproductas a payload.
- Map the
dispatchfunction to theProductDescriptioncomponent's props. - Define a
handleAddToCartfunction that takes aproduct objectas an argument and dispatch theaddProductaction creator with thatproduct. - When you click on the add to cart button, the
onSubmitEventListener catches that and calls thehandleAddToCartwith the correspondent details for that product as an object. - The
idfor each object is generated with thegenerateIDhelper function that takes the product id and its selected attributes and returns the concatenation of those two. - The reason why I produce a unique
idlike this, is to avoid having the same id if a user added the same product with different attributes. - You can't add a product if the attributes are not selected
- It follows a similar pattern as from adding the description page
- The only difference is instead of having
onSubmitEventListener, it has anonCLickEventListener that fires whenever you click the cart icon. - Because a user can't add a product to the cart without selecting attributes, I created the
getFirstAttrsfunction that selects the first attributes as a default.
- Create
addProductOrIncreaseQauntitythat takes the items already existing inside the cart and theidof the product added coming as payload, and it returns a new array. - If the same product is found it increases its quantity, if not it will add a new product to the state.
- Because of the way the
idis generated (original id + attributes), if a product is added with different attributes it will count as a new product.
- Create the
<CartItems />component and connect its props with the redux store,cartItems. Then output all the items. - Create a
<Carousel />component for the gallery. You can click the left and right arrows to navigate between images. - create a
<Productquantity />component to allow the user to update the product quantity.
- Define the
updateQuantityaction creator that takes a quantity and a product id, and returns an action with those two as a payload. - Whenever a user changes the quantity of a product on the cart page it will dispatch the
updateQuantityaction. - In the cartReducer handle the
UPDATE_QUANTITYcase, it maps through the items in the state and changes the quantity of the product that its id matches theidcoming as payload.
- it follows the same pattern as updating the quantity, the only difference is that it uses
.filterto delete the product that matches the id coming as a payload
- create an overlay and use the
<CartItems />in inside it. Pass a propertyoverlayof true to style it differently than the cart page. - Add
<QuantityBadge />component and connect its props to the redux state. - It uses the array method
.reduceto iterate through the items in the cart and output the quantity of the total items.
- I didn't use redux to store the data I'm getting back (products for example) from the GraphQL endpoint. Because Apollo Client kind of doing the same with the
inMemoryCacheand they are planning to make Apollo to be the only source of truth and "replace" redux. - I used ES6 arrow functions so I don't hav e to bind my methods (functions) everytime.