{"id":19261582,"url":"https://github.com/emilydaykin/gifter","last_synced_at":"2026-04-16T10:03:31.847Z","repository":{"id":256217179,"uuid":"506582685","full_name":"emilydaykin/Gifter","owner":"emilydaykin","description":"🎁 A full-stack, tested \u0026 responsive e-commerce site to browse and buy gifts for any occasion","archived":false,"fork":false,"pushed_at":"2024-10-31T11:23:53.000Z","size":49526,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-05T10:09:38.603Z","etag":null,"topics":["firebase","firestore-database","functional-programming","jest","react","react-testing-library","redux","snapshot-testing","stripe"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/emilydaykin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-06-23T09:47:57.000Z","updated_at":"2022-08-12T19:21:03.000Z","dependencies_parsed_at":"2024-09-09T17:35:05.434Z","dependency_job_id":null,"html_url":"https://github.com/emilydaykin/Gifter","commit_stats":null,"previous_names":["emilydaykin/gifter"],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emilydaykin%2FGifter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emilydaykin%2FGifter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emilydaykin%2FGifter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/emilydaykin%2FGifter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/emilydaykin","download_url":"https://codeload.github.com/emilydaykin/Gifter/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240358544,"owners_count":19788916,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["firebase","firestore-database","functional-programming","jest","react","react-testing-library","redux","snapshot-testing","stripe"],"created_at":"2024-11-09T19:27:38.453Z","updated_at":"2026-04-16T10:03:26.826Z","avatar_url":"https://github.com/emilydaykin.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gifter\n\nA full-stack, tested \u0026 **responsive** e-commerce site to browse and buy gifts for any occasion. Gifter is built with **TypeScript**, **JavaScript**, **React**, **Redux**, **Sass**, **Stripe** and **Firebase**. Users can browse from 100 gifts across 5 categories, add, edit and remove items from their cart, and checkout and pay (with test card details). Gifter works on any device size, and has been tested to minimise bugs (**22 tests across 7 test suites**, and **4 snapshots**).\n\n[Live Gifter App](https://giftsbygifter.netlify.app/)\n\n## Application Walkthrough \n\n#### Home Page\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/home.png\" width=\"90%\"  /\u003e\n\u003c/p\u003e\n\n#### About Page\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/about.png\" width=\"90%\"  /\u003e\n\u003c/p\u003e\n\n#### Shop Overview (desktop \u0026 mobile)\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/shop.gif\" width=\"70%\"  /\u003e\n  \u003cimg src=\"src/assets/readme/shop_mobile.gif\" width=\"27.25%\"  /\u003e\n\u003c/p\u003e\n\n#### Authentication Page (desktop \u0026 mobile)\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/auth.png\" width=\"65%\"  /\u003e\n  \u003cimg src=\"src/assets/readme/auth_mobile.png\" width=\"25.3%\"  /\u003e\n\u003c/p\u003e\n\n#### Shop Category Page\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/shop_category.gif\" width=\"90%\"  /\u003e\n\u003c/p\u003e\n\n#### Checkout (desktop \u0026 mobile)\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/cart_checkout.gif\" width=\"71%\"  /\u003e\n  \u003cimg src=\"src/assets/readme/checkout_mobile.gif\" width=\"27.57%\"  /\u003e\n\u003c/p\u003e\n\n#### Payment\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"src/assets/readme/payment.gif\" width=\"90%\"  /\u003e\n\u003c/p\u003e\n\n## Tech Stack\n- Front End: \n  - JavaScript \u0026 Typescript\n  - React (Hooks: useState, useEffect, useContext, useReducer, useCallback)\n  - Redux (including Redux Thunk \u0026 Redux Saga for asynchronous redux side effect handling)\n  - Functional Programming Design Patterns: Currying, Memoisation (via Redux's Reselect library)\n  - Sass (BEM)\n- Back End:\n  - Authentication: Firebase\n  - Server \u0026 Storage: Firestore\n  - Serverless Functions\n  - Payment Gateway: Stripe\n- DevOps:\n  - Deployment: Netlify\n  - Testing \n    - Testing Library (jest-dom, React, user-event)\n    - Jest (\u0026 Snapshot testing for static/stateless components)\n    - (Enzyme: will attempt to convert to enzyme once React 18 is supported)\n  - Yarn\n\nI also created a [spin-off version of Gifter](https://github.com/emilydaykin/graphql) that leverages GraphQL and Apollo.\n\n## Features:\n- Display of 5 gift categories (Birthday, Chirstmas, Thank you, Anniversary and Wedding)\n- Authentication by email and password, or with Google\n- Add/Remove item(s) to/from basket with a real time item counter and price total calculator\n- Payment with Stripe\n- Fully responsive for any device size\n\n\n## Milestones:\nThis project went though a few refactors and improvements as I learnt new libraries, frameworks and languages to incorporate. Using `git tag -a \u003cversion\u003e -m \"\u003cversion comments\u003e\"` to mark each of these in the code history ([see all tags](https://github.com/emilydaykin/Gifter/tags)), the state of Gifter at each milestone was as follows:\n\n#### (v6: Coming soon - Gifter to be a PWA (progressive web app))\n\n### [v5](https://github.com/emilydaykin/Gifter/releases/tag/v5)\n- Testing (React Testing Library / Jest / Snapshot Testing)\n### [v4](https://github.com/emilydaykin/Gifter/releases/tag/v4)\n- Performance optimisations (useCallback and React memo for function and function output memoisations respectively, and code splitting (the bundle.js) with dynamic imports via React Lazy \u0026 React Suspense)\n- Tightening Firebase (Firestore) security rules to read-only for all documents and categories, and allowing write access for users if the id matches the request's.\n### [v3](https://github.com/emilydaykin/Gifter/releases/tag/v3)\n- Codebase converted from JavaScript to TypeScript, including React Components, the entire Redux Store (and Sagas), and utility files (for firebase and reducer)\n### [v2](https://github.com/emilydaykin/Gifter/releases/tag/v2) \n- Redux (Redux Saga \u0026 Generator functions) and Stripe integration\n- Serverless Function that creates a payment intent for Stripe. It is hosted on Netlify and uses AWS' Lambda function under the hood. This will help automate any necessary scaling.\n- Currying \u0026 Memoisation Design Patterns (via Redux's Reselect library)\n- Session Storage via Redux Persist to retain data between refreshes/sessions.\n- UX: Users can now pay for their selected gifts using a test credit card number, which will be handled by Stripe.\n### [v1](https://github.com/emilydaykin/Gifter/releases/tag/v1)\n- Fully working and responsive app in web, tablet and mobile, powered by JavaScript and React, including useContext and useReducer Hooks. \n- Styling done in pure Sass (without the help of any frameworks) using the BEM methodology.\n- Server, Storage and Authentication handled by Firebase (\u0026 Firestore).\n- UX: Users can browse gifts across 5 categories, sign in (with email or via Google) and sign out, as well as add to, edit and remove items from their cart; they can also check out, but can't yet pay.\n\n\n## Tests:\n\u003cp align=\"left\"\u003e\n  \u0026emsp;\n  \u003cimg src=\"src/assets/readme/tests.png\" width=\"60%\"  /\u003e\n\u003c/p\u003e\n\n## Code Snippets:\n\n### Testing Reducers\n\u003cdetails\u003e\n  \u003csummary\u003eView tests\u003c/summary\u003e\n  \n  ```javascript\n  import * as cartReducers from '../store/cart/cart.reducer';\n  import * as cartTypes from '../store/cart/cart.types';\n\n  const mockCartItem = {\n    id: 35,\n    name: 'Smart Watch - Track your steps, calories, sleep and more',\n    imageUrl:\n      'https://images.unsplash.com/photo-1508685096489-7aacd43bd3b1?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8c21hcnQlMjB3YXRjaHxlbnwwfHwwfHw%3D\u0026auto=format\u0026fit=crop\u0026w=800\u0026q=60',\n    price: 105\n  };\n\n  test('Cart reducer returns correct initial state', () =\u003e {\n    expect(cartReducers.cartReducer(undefined, {})).toEqual(cartReducers.CART_INITIAL_STATE);\n  });\n\n  test('Cart reducer sets cart items correctly', () =\u003e {\n    expect(\n      cartReducers.cartReducer(cartReducers.CART_INITIAL_STATE, {\n        type: cartTypes.CART_ACTION_TYPES.SET_CART_ITEMS,\n        payload: mockCartItem\n      }).cartItems\n    ).toEqual(mockCartItem);\n\n    expect(\n      cartReducers.cartReducer(cartReducers.CART_INITIAL_STATE, {\n        type: cartTypes.CART_ACTION_TYPES.SET_CART_ITEMS,\n        payload: mockCartItem\n      }).isCartOpen\n    ).toEqual(false);\n  });\n  ```\n\u003c/details\u003e\n\n### Improving Firebase (Firestore) Security Rules\n\u003cdetails\u003e\n  \u003csummary\u003eView rule\u003c/summary\u003e\n  \n  ```\n  rules_version = '2';\n  service cloud.firestore {\n    match /databases/{database}/documents {\n      match /{document=**} {\n        allow read;\n      }\n      \n      match /users/{userId} {\n        allow read, get, create;\n        allow write: if request.auth != null \u0026\u0026 request.auth.id == userId;\n      }\n      \n      match /categories/{category} {\n        allow read;\n      }\n    }\n  }\n  ```\n\u003c/details\u003e\n\n### Dynamic Imports via React Lazy and React Suspense for performance optimisation\n\u003cdetails\u003e\n  \u003csummary\u003eView Code\u003c/summary\u003e\n  \n  ```javascript\n  // $src/App.js\n\n  const Home = lazy(() =\u003e import('./components/Home'));\n  const Navbar = lazy(() =\u003e import('./components/Navbar'));\n  const About = lazy(() =\u003e import('./components/About'));\n  const Shop = lazy(() =\u003e import('./components/Shop'));\n  const SignIn = lazy(() =\u003e import('./components/auth/SignIn'));\n  const Checkout = lazy(() =\u003e import('./components/checkout/Checkout'));\n\n  const App = () =\u003e {\n    ...\n\n    return (\n      \u003cSuspense fallback={\u003cLoader /\u003e}\u003e\n        \u003cRoutes\u003e\n          \u003cRoute path='/' element={\u003cNavbar /\u003e}\u003e\n            \u003cRoute index element={\u003cHome /\u003e} /\u003e\n            \u003cRoute path='shop/*' element={\u003cShop /\u003e} /\u003e\n            \u003cRoute path='about' element={\u003cAbout /\u003e} /\u003e\n            \u003cRoute path='auth' element={\u003cSignIn /\u003e} /\u003e\n            \u003cRoute path='checkout' element={\u003cCheckout /\u003e} /\u003e\n          \u003c/Route\u003e\n        \u003c/Routes\u003e\n      \u003c/Suspense\u003e\n    );\n  };\n  ```\n\u003c/details\u003e\n\n### UseCallback hook to optimise performance by memoising functions\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Go To Checkout callback)\u003c/summary\u003e\n  \n  ```javascript\n  const goToCheckout = useCallback(() =\u003e {\n    if (cartItems.length \u003e 0) {\n      navigate('/checkout');\n      dispatch(setIsCartOpen(!isCartOpen));\n    }\n  }, [isCartOpen]);\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Redirecting to Target Category callback)\u003c/summary\u003e\n  \n  ```javascript\n  const redirectToCategory = useCallback((category: string) =\u003e {\n    navigate(`/shop/${category}`);\n  }, []);\n  ```\n\u003c/details\u003e\n\n\n### TypeScript\n\u003cdetails\u003e\n  \u003csummary\u003eShop Data in TypeScript\u003c/summary\u003e\n  \n  ```typescript\n  const shopData: {\n    title: String;\n    items: {\n      id: Number;\n      name: String;\n      imageUrl: String;\n      price: Number;\n    }[];\n  }[] = [\n    {\n      title: 'Christmas',\n      items: [\n        {\n          id: 1,\n          name: '4-Piece Stocking',\n          imageUrl:\n            'https://images.unsplash.com/photo-1607900177462-ac553f1f5d97?ixlib=rb-1.2.1\u0026ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8Y2hyaXN0bWFzJTIwc3RvY2tpbmdzfGVufDB8fDB8fA%3D%3D\u0026auto=format\u0026fit=crop\u0026w=800\u0026q=60',\n          price: 12\n        },\n        ...\n      ]\n      ...\n    }\n    ...\n  ]\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eCart Reducer in TypeScript\u003c/summary\u003e\n  \n  ```typescript\n  import { AnyAction } from 'redux';\n  import { setCartItems, setIsCartOpen } from './cart.action';\n  import { CartItem } from './cart.types';\n\n  export type CartState = {\n    readonly isCartOpen: boolean;\n    readonly cartItems: CartItem[];\n  };\n\n  export const CART_INITIAL_STATE: CartState = {\n    isCartOpen: false,\n    cartItems: []\n  };\n\n  export const cartReducer = (state = CART_INITIAL_STATE, action: AnyAction): CartState =\u003e {\n    if (setIsCartOpen.match(action)) {\n      return {\n        ...state,\n        isCartOpen: action.payload\n      };\n    } else if (setCartItems.match(action)) {\n      return {\n        ...state,\n        cartItems: action.payload\n      };\n    } else {\n      return state;\n    }\n  };\n\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eCategory Types in TypeScript\u003c/summary\u003e\n  \n  ```typescript\n  export enum CATEGORIES_ACTION_TYPES {\n    FETCH_CATEGORIES_START = 'category/FETCH_CATEGORIES_START',\n    FETCH_CATEGORIES_SUCCESS = 'category/FETCH_CATEGORIES_SUCCESS',\n    FETCH_CATEGORIES_FAILURE = 'category/FETCH_CATEGORIES_FAILURE'\n  }\n\n  export type CategoryItem = {\n    id: number;\n    imageUrl: string;\n    name: string;\n    price: number;\n  };\n\n  export type Category = {\n    title: string;\n    imageUrl: string;\n    items: CategoryItem[];\n  };\n\n  export type CategoryMap = {\n    [key: string]: CategoryItem[];\n  };\n\n\n  ```\n\u003c/details\u003e\n\n\n\n### Generator Functions \u0026 Redux Saga for Categories\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Root Saga)\u003c/summary\u003e\n  \n  ```javascript\n  import { all, call } from 'redux-saga/effects';\n  import { categoriesSaga } from './categories/category.saga';\n  import { userSaga } from './user/user.saga';\n\n  // generator function\n  export function* rootSaga() {\n    yield all([call(categoriesSaga), call(userSaga)]);\n  }\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Category Saga)\u003c/summary\u003e\n  \n  ```javascript\n  import { takeLatest, all, call, put } from 'redux-saga/effects';\n  import { getCategoriesAndDocuments } from '../../firebase/firebase.utils';\n  import { fetchCategoriesSuccess, fetchCategoriesFailure } from './category.action';\n  import { CATEGORIES_ACTION_TYPES } from './category.types';\n\n  // Generators:\n  export function* fetchCategoriesAsync() {\n    try {\n      // use `call` to turn it into an effect\n      const categoryArray = yield call(getCategoriesAndDocuments, 'categories'); // callable method \u0026 its params\n      yield put(fetchCategoriesSuccess(categoryArray)); // put is the dispatch inside a generator\n    } catch (err) {\n      // console.log(`ERROR: ${err}`);\n      yield put(fetchCategoriesFailure(err));\n    }\n  }\n\n  export function* onFetchCategories() {\n    // if many actions received, take the latest one\n    yield takeLatest(CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_START, fetchCategoriesAsync);\n  }\n\n  export function* categoriesSaga() {\n    yield all([call(onFetchCategories)]); // this will pause execution of the below until it finishes\n  }\n  ```\n\u003c/details\u003e\n\n### Redux Thunk for Categories\n\u003cdetails\u003e\n  \u003csummary\u003eView Code\u003c/summary\u003e\n  \n  ```javascript\n  import { CATEGORIES_ACTION_TYPES } from './category.types';\n  import { getCategoriesAndDocuments } from '../../firebase/firebase.utils';\n\n  export const fetchCategoriesStart = () =\u003e {\n    return { type: CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_START };\n  };\n\n  export const fetchCategoriesSuccess = (categories) =\u003e {\n    return { type: CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_SUCCESS, payload: categories };\n  };\n\n  export const fetchCategoriesFailure = (error) =\u003e {\n    return { type: CATEGORIES_ACTION_TYPES.FETCH_CATEGORIES_FAILURE, payload: error };\n  };\n\n  // Thunk:\n  export const fetchCategoriesAsync = () =\u003e async (dispatch) =\u003e {\n    dispatch(fetchCategoriesStart());\n    try {\n      const categoryArray = await getCategoriesAndDocuments('categories');\n      dispatch(fetchCategoriesSuccess(categoryArray));\n    } catch (error) {\n      // console.log(`ERROR: ${error}`);\n      dispatch(fetchCategoriesFailure(error));\n    }\n  };\n  ```\n\u003c/details\u003e\n\n### React Context: useContext hook and CartContext and UserContext in the Navbar \u0026rarr; later refactored to Redux.\n\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Navbar)\u003c/summary\u003e\n  \n  ```javascript\n  // $src/components/Navbar.jsx\n\n  import { useContext } from 'react';\n  import { UserContext } from '../contexts/user.context';\n  import { CartContext } from '../contexts/cart.context';\n  \n  const Navbar = () =\u003e {\n    const { currentUser } = useContext(UserContext);\n    const { isCartOpen, setIsCartOpen } = useContext(CartContext);\n    const toggleShowHideCart = () =\u003e setIsCartOpen(!isCartOpen);\n    const location = useLocation();\n\n    const hideCartWhenNavigatingAway = () =\u003e {\n      if (isCartOpen) {\n        setIsCartOpen(!isCartOpen);\n      }\n    };\n    ...\n  }\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (User Context)\u003c/summary\u003e\n  \n  ```javascript\n  // $src/contexts/user.context.jsx\n\n  export const UserContext = createContext({\n    currentUser: null,\n    setCurrentUser: () =\u003e null\n  });\n\n  export const UserProvider = ({ children }) =\u003e {\n    const [currentUser, setCurrentUser] = useState(null);\n    const value = { currentUser, setCurrentUser };\n\n    useEffect(() =\u003e {\n      const unsubscribe = onAuthStateChangeListener((user) =\u003e {\n        if (user) {\n          createUserDocumentFromAuth(user);\n        }\n        setCurrentUser(user);\n      });\n      return unsubscribe;\n    }, []);\n\n    return \u003cUserContext.Provider value={value}\u003e{children}\u003c/UserContext.Provider\u003e;\n  };\n  ```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eView Code (Cart Context)\u003c/summary\u003e\n  \n  ```javascript\n  // $src/contexts/cart.context.jsx\n  \n  export const CartContext = createContext({\n    isCartOpen: false,\n    setIsCartOpen: () =\u003e {},\n    cartItems: [],\n    addItemToCart: () =\u003e {},\n    removeItemFromCart: () =\u003e {},\n    reduceItemQuantityInCart: () =\u003e {},\n    getCartItemCount: () =\u003e {},\n    getCartTotalPrice: () =\u003e {}\n  });\n\n  export const CartProvider = ({ children }) =\u003e {\n    const [isCartOpen, setIsCartOpen] = useState(false);\n    const [cartItems, setCartItems] = useState([]);\n\n    const addItemToCart = (productToAdd) =\u003e {\n      const matchingItemIndex = cartItems.findIndex((item) =\u003e item.id === productToAdd.id);\n\n      if (matchingItemIndex === -1) {\n        setCartItems([...cartItems, { ...productToAdd, quantity: 1 }]);\n      } else {\n        const updatedCartItems = cartItems.map((item) =\u003e {\n          return item.id === productToAdd.id ? { ...item, quantity: item.quantity + 1 } : item;\n        });\n        setCartItems(updatedCartItems);\n      }\n    };\n\n    const removeItemFromCart = (productToRemove) =\u003e {\n      const updatedCartItems = cartItems.filter((item) =\u003e item.id !== productToRemove.id);\n      setCartItems(updatedCartItems);\n    };\n\n    const reduceItemQuantityInCart = (productToReduce) =\u003e {\n      const quantityOfItem = productToReduce.quantity;\n\n      const reduceQuantity = cartItems.map((item) =\u003e {\n        return item.id === productToReduce.id ? { ...item, quantity: item.quantity - 1 } : item;\n      });\n\n      const removeItem = cartItems.filter((item) =\u003e item.id !== productToReduce.id);\n\n      setCartItems(quantityOfItem \u003e 1 ? reduceQuantity : removeItem);\n    };\n\n    const getCartItemCount = () =\u003e {\n      return cartItems.reduce((prev, curr) =\u003e prev + curr.quantity, 0);\n    };\n\n    const getCartTotalPrice = () =\u003e {\n      const total = cartItems.reduce((prev, curr) =\u003e prev + curr.price * curr.quantity, 0);\n      return total % 1 \u003e 0 ? total.toFixed(2) : total; // currency rounding:\n    };\n    ...\n  }\n  ```\n\u003c/details\u003e\n\n\n\n## Challenges, Wins \u0026 Key Learning\n\n### Challenges:\n- Biggest challenge: Redux Saga (a lot of boilerplate set up and config to learn)\n- TypeScript for Redux\n\n### Wins\n- First time integrating a payment gateway\n- Design (horizontal scroll with fade out effects on the side) on Shop Overview page\n\n### Key Learnings:\n- In testing, `waitFor` (or other React Testing Library (RTL) async utilities such as `waitForElementToBeRemoved` or `findBy`) may be better practice than wrapping renders with `act()` because:\n  1. RTL already wraps utilities in `act()`\n  2. `act()` will supress the warnings and make the test past but cause other issues","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femilydaykin%2Fgifter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femilydaykin%2Fgifter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femilydaykin%2Fgifter/lists"}