From 76044105100f7da87ad696bb9219e8863b38069f Mon Sep 17 00:00:00 2001 From: Pranav Kural Date: Thu, 12 Sep 2024 17:27:17 -0400 Subject: [PATCH] Code to handle synchronization between local and DB user cart data (#8) * Code to handle synchronization between local and DB user cart data * Updated file names of component files * Reverted file name changes to fix issue with Vercel build * Synchronizing cart data mutations with the backend * Refactored code & re-orgnanized project structure to follow standard guidelines --- app/(data)/sample-data.ts | 2 +- app/(routes)/category/[id]/page.tsx | 4 +- .../[id]/subcategory/[subCategoryId]/page.tsx | 4 +- .../product/[productId]/page.tsx | 4 +- app/(routes)/checkout/page.tsx | 8 +- .../addresses/SelectAddressModal.tsx | 2 +- app/components/auth/SigInSignUpButtons.tsx | 32 +++ app/components/cart/AddToCartButton.tsx | 103 +++++++++ app/components/cart/CartButton.tsx | 42 ++++ app/components/cart/CartIcon.tsx | 44 ++++ app/components/cart/CartWindow.tsx | 122 ++++++++++ app/components/cart/CheckoutCartItems.tsx | 36 +++ app/components/cart/MergeCartItems.tsx | 209 ++++++++++++++++++ app/components/cart/MutateCartButton.tsx | 144 ++++++++++++ ...rCartButton.tsx => StatefulCartButton.tsx} | 19 +- app/components/cart/UserCartButton.tsx | 62 ++++++ app/components/cart/addToCartButton.tsx | 55 +++-- app/components/cart/cartButton.tsx | 46 ++-- app/components/cart/cartIcon.tsx | 2 +- app/components/cart/cartWindow.tsx | 13 +- app/components/cart/checkoutCartItems.tsx | 4 +- app/components/cart/mutateCartButton.tsx | 60 ++++- app/components/cart/userCartButton.tsx | 8 +- app/components/checkout/AddNewOrder.tsx | 25 ++- app/components/checkout/AddressesSection.tsx | 2 +- .../checkout/CheckoutPaymentModal.tsx | 11 +- app/components/checkout/OrderSection.tsx | 4 +- .../checkout/UserDetailsSection.tsx | 2 +- app/components/error/ErrorFetchingData.tsx | 41 ++++ app/components/header/Header.tsx | 30 +++ .../header/breadcrumb/Breadcrumb.tsx | 62 ++++++ .../header/breadcrumb/CategoryPart.tsx | 51 +++++ .../header/breadcrumb/ProductPart.tsx | 51 +++++ .../header/breadcrumb/SubCategoryPart.tsx | 48 ++++ .../header/breadcrumb/breadcrumb.tsx | 6 +- app/components/header/header.tsx | 8 +- .../nav/{list-item.tsx => ListItem.tsx} | 0 .../header/nav/{nav-menu.tsx => NavMenu.tsx} | 2 +- .../{nav-menu-item.tsx => NavMenuItem.tsx} | 2 +- app/components/landing/CreateUser.tsx | 6 +- app/components/landing/Showcase.tsx | 24 ++ app/components/landing/showcase.tsx | 2 +- app/components/orders/DetailedOrdersView.tsx | 2 +- app/components/orders/OrderAddress.tsx | 2 +- app/components/orders/OrderDetailsTile.tsx | 6 +- app/components/orders/OrderItemTile.tsx | 4 +- .../products/ProductCheckoutPreview.tsx | 64 ++++++ app/components/products/ProductPreview.tsx | 117 ++++++++++ app/components/products/ProductTile.tsx | 59 +++++ app/components/products/ProductsShowcase.tsx | 42 ++++ .../ProductsShowcaseWithSubcategories.tsx | 96 ++++++++ .../products/productCheckoutPreview.tsx | 4 +- app/components/products/productPreview.tsx | 4 +- app/components/products/productTile.tsx | 4 +- app/components/products/productsShowcase.tsx | 6 +- .../productsShowcaseWithSubcategories.tsx | 6 +- app/components/toast/Toast.tsx | 32 +++ app/components/ui/loading-spinner.tsx | 2 + app/layout.tsx | 2 +- app/lib/api/api-slice.ts | 2 +- app/lib/api/cart-items-slice.ts | 42 +++- app/lib/api/categories-slice.ts | 11 +- app/lib/api/orders-slice.ts | 13 +- app/lib/api/products-api-slice.ts | 9 +- app/lib/api/users-slice.ts | 18 +- app/lib/hooks/useCartHooks.ts | 14 -- app/lib/plamatio-backend/actions.ts | 7 +- .../{plamatio-api.ts => endpoints.ts} | 33 +-- app/lib/plamatio-backend/types.ts | 110 ++++----- app/lib/plamatio-backend/types/cart-types.ts | 32 +++ .../plamatio-backend/types/category-types.ts | 22 ++ app/lib/plamatio-backend/types/order-types.ts | 71 ++++++ .../plamatio-backend/types/product-types.ts | 21 ++ app/lib/plamatio-backend/types/user-types.ts | 41 ++++ app/lib/plamatio-backend/utils.ts | 21 ++ app/lib/store/reducers/cart/cartReducer.ts | 23 +- app/lib/store/reducers/utils.ts | 1 - app/lib/stripe/utils.ts | 7 +- app/page.tsx | 2 +- app/types/backend-types.ts | 99 --------- app/types/types.ts | 8 - 81 files changed, 2063 insertions(+), 398 deletions(-) create mode 100644 app/components/auth/SigInSignUpButtons.tsx create mode 100644 app/components/cart/AddToCartButton.tsx create mode 100644 app/components/cart/CartButton.tsx create mode 100644 app/components/cart/CartIcon.tsx create mode 100644 app/components/cart/CartWindow.tsx create mode 100644 app/components/cart/CheckoutCartItems.tsx create mode 100644 app/components/cart/MergeCartItems.tsx create mode 100644 app/components/cart/MutateCartButton.tsx rename app/components/cart/{noUserCartButton.tsx => StatefulCartButton.tsx} (80%) create mode 100644 app/components/cart/UserCartButton.tsx create mode 100644 app/components/error/ErrorFetchingData.tsx create mode 100644 app/components/header/Header.tsx create mode 100644 app/components/header/breadcrumb/Breadcrumb.tsx create mode 100644 app/components/header/breadcrumb/CategoryPart.tsx create mode 100644 app/components/header/breadcrumb/ProductPart.tsx create mode 100644 app/components/header/breadcrumb/SubCategoryPart.tsx rename app/components/header/nav/{list-item.tsx => ListItem.tsx} (100%) rename app/components/header/nav/{nav-menu.tsx => NavMenu.tsx} (98%) rename app/components/header/nav/{nav-menu-item.tsx => NavMenuItem.tsx} (98%) create mode 100644 app/components/landing/Showcase.tsx create mode 100644 app/components/products/ProductCheckoutPreview.tsx create mode 100644 app/components/products/ProductPreview.tsx create mode 100644 app/components/products/ProductTile.tsx create mode 100644 app/components/products/ProductsShowcase.tsx create mode 100644 app/components/products/ProductsShowcaseWithSubcategories.tsx create mode 100644 app/components/toast/Toast.tsx delete mode 100644 app/lib/hooks/useCartHooks.ts rename app/lib/plamatio-backend/{plamatio-api.ts => endpoints.ts} (78%) create mode 100644 app/lib/plamatio-backend/types/cart-types.ts create mode 100644 app/lib/plamatio-backend/types/category-types.ts create mode 100644 app/lib/plamatio-backend/types/order-types.ts create mode 100644 app/lib/plamatio-backend/types/product-types.ts create mode 100644 app/lib/plamatio-backend/types/user-types.ts create mode 100644 app/lib/plamatio-backend/utils.ts delete mode 100644 app/lib/store/reducers/utils.ts delete mode 100644 app/types/backend-types.ts delete mode 100644 app/types/types.ts diff --git a/app/(data)/sample-data.ts b/app/(data)/sample-data.ts index 941ce39..e339db0 100644 --- a/app/(data)/sample-data.ts +++ b/app/(data)/sample-data.ts @@ -3,7 +3,7 @@ import { SubCategory, Product, CategoryHeroProduct, -} from '../types/backend-types'; +} from '@/app/lib/plamatio-backend/types'; const categories: Category[] = [ { diff --git a/app/(routes)/category/[id]/page.tsx b/app/(routes)/category/[id]/page.tsx index b0b6c66..8fd5d97 100644 --- a/app/(routes)/category/[id]/page.tsx +++ b/app/(routes)/category/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import ErrorFetchingData from '@/app/components/error/errorFetchingData'; -import ProductsShowcase from '@/app/components/products/productsShowcase'; +import ErrorFetchingData from '@/app/components/error/ErrorFetchingData'; +import ProductsShowcase from '@/app/components/products/ProductsShowcase'; import {LoadingSpinner} from '@/app/components/ui/loading-spinner'; import {useGetHeroProductsByCategoryQuery} from '@/app/lib/api/products-api-slice'; import {useMemo} from 'react'; diff --git a/app/(routes)/category/[id]/subcategory/[subCategoryId]/page.tsx b/app/(routes)/category/[id]/subcategory/[subCategoryId]/page.tsx index 4846342..b0f8478 100644 --- a/app/(routes)/category/[id]/subcategory/[subCategoryId]/page.tsx +++ b/app/(routes)/category/[id]/subcategory/[subCategoryId]/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import ErrorFetchingData from '@/app/components/error/errorFetchingData'; -import ProductsShowcase from '@/app/components/products/productsShowcase'; +import ErrorFetchingData from '@/app/components/error/ErrorFetchingData'; +import ProductsShowcase from '@/app/components/products/ProductsShowcase'; import {LoadingSpinner} from '@/app/components/ui/loading-spinner'; import {useGetProductsBySubCategoryQuery} from '@/app/lib/api/products-api-slice'; import {useMemo} from 'react'; diff --git a/app/(routes)/category/[id]/subcategory/[subCategoryId]/product/[productId]/page.tsx b/app/(routes)/category/[id]/subcategory/[subCategoryId]/product/[productId]/page.tsx index dd73aa7..d3176f3 100644 --- a/app/(routes)/category/[id]/subcategory/[subCategoryId]/product/[productId]/page.tsx +++ b/app/(routes)/category/[id]/subcategory/[subCategoryId]/product/[productId]/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import ErrorFetchingData from '@/app/components/error/errorFetchingData'; -import {ProductPreview} from '@/app/components/products/productPreview'; +import ErrorFetchingData from '@/app/components/error/ErrorFetchingData'; +import {ProductPreview} from '@/app/components/products/ProductPreview'; import {LoadingSpinner} from '@/app/components/ui/loading-spinner'; import {useGetProductQuery} from '@/app/lib/api/products-api-slice'; import {FetchBaseQueryError} from '@reduxjs/toolkit/query'; diff --git a/app/(routes)/checkout/page.tsx b/app/(routes)/checkout/page.tsx index 0f4136e..c0495d0 100644 --- a/app/(routes)/checkout/page.tsx +++ b/app/(routes)/checkout/page.tsx @@ -2,15 +2,15 @@ import {useEffect, useMemo, useState} from 'react'; import {selectCartItems} from '@/app/lib/store/reducers/cart/cartReducer'; import {useAppSelector} from '@/app/lib/store/storeHooks'; -import {CheckoutCartItems} from '@/app/components/cart/checkoutCartItems'; +import {CheckoutCartItems} from '@/app/components/cart/CheckoutCartItems'; import {Raleway} from 'next/font/google'; import {useGetProductsQuery} from '@/app/lib/api/products-api-slice'; -import {Product, User} from '@/app/types/backend-types'; +import {Product, User} from '@/app/lib/plamatio-backend/types'; import {LoadingSpinner} from '../../components/ui/loading-spinner'; import UserDetailsSection from '../../components/checkout/UserDetailsSection'; import AddressesSection from '../../components/checkout/AddressesSection'; import {useUser} from '@clerk/nextjs'; -import SignInSignUpButtons from '../../components/auth/sigInSignUpButtons'; +import SignInSignUpButtons from '../../components/auth/SigInSignUpButtons'; import CheckoutPaymentModal from '../../components/checkout/CheckoutPaymentModal'; import OrderSection from '../../components/checkout/OrderSection'; @@ -44,7 +44,7 @@ export default function CheckoutPage() { useEffect(() => { if (productsFetch.isSuccess) { const products = productsFetch.data?.data.filter((product) => - cartItems.map((item) => item.productId).includes(product.id) + cartItems.map((item) => item.product_id).includes(product.id) ); if (products) { setProductsInCart(products); diff --git a/app/components/addresses/SelectAddressModal.tsx b/app/components/addresses/SelectAddressModal.tsx index d00a728..5cb530a 100644 --- a/app/components/addresses/SelectAddressModal.tsx +++ b/app/components/addresses/SelectAddressModal.tsx @@ -1,7 +1,7 @@ import React, {FC, useMemo} from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import {TrashIcon, XIcon} from 'lucide-react'; -import {Address} from '@/app/types/backend-types'; +import {Address} from '@/app/lib/plamatio-backend/types'; import {useDeleteUserAddressMutation} from '@/app/lib/api/users-slice'; import {LoadingSpinner} from '../ui/loading-spinner'; diff --git a/app/components/auth/SigInSignUpButtons.tsx b/app/components/auth/SigInSignUpButtons.tsx new file mode 100644 index 0000000..d9f1011 --- /dev/null +++ b/app/components/auth/SigInSignUpButtons.tsx @@ -0,0 +1,32 @@ +import {SignedIn, SignedOut, SignOutButton} from '@clerk/nextjs'; +import {User2Icon} from 'lucide-react'; +import Link from 'next/link'; + +export const SignInSignUpButtons = () => { + return ( +
+ + + Sign in + + + Sign up + + + + +
+ Sign Out + +
+
+
+
+ ); +}; + +export default SignInSignUpButtons; diff --git a/app/components/cart/AddToCartButton.tsx b/app/components/cart/AddToCartButton.tsx new file mode 100644 index 0000000..e77e86c --- /dev/null +++ b/app/components/cart/AddToCartButton.tsx @@ -0,0 +1,103 @@ +'use client'; +import {PlusIcon, ShoppingCartIcon} from 'lucide-react'; +import classNames from 'classnames'; +import {FC, useEffect, useMemo, useState} from 'react'; +import {Toast} from '../toast/Toast'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; +import {useAppDispatch} from '@/app/lib/store/storeHooks'; +import {addCartItem} from '@/app/lib/store/reducers/cart/cartReducer'; +import {useAddCartItemMutation} from '@/app/lib/api/cart-items-slice'; + +type AddToCartButtonProps = { + product: Product; + userId?: string; + showLabel?: boolean; + className?: string; + labelClassName?: string; +}; + +export const AddToCartButton: FC = ( + props: AddToCartButtonProps +) => { + const [toastVisible, setToastVisible] = useState(false); + // dispatch cart actions + const dispatch = useAppDispatch(); + // when user logged in, need to add cart item to database + const [addCartItemToDB, {isError, error}] = useAddCartItemMutation(); + + // Log any error in updating cart item on database + useMemo(() => { + if (isError) { + console.error(`AddToCartButton: error adding cart item: ${error}`); + } + }, [isError, error]); + + useEffect(() => { + if (toastVisible) { + setTimeout(() => { + setToastVisible(false); + }, 1500); + } + }, [toastVisible]); + + const handleAddToCart = async () => { + // prepare cart item + const cartItemToAdd: CartItem = { + id: Math.floor(Math.random() * 10000), + product_id: props.product.id, + quantity: 1, + user_id: props.userId && props.userId.length > 0 ? props.userId : '', // no user id if not logged in + }; + + // if valid user id, add cart item to database + if (props.userId && props.userId.length > 0) { + console.log( + `AddToCartButton: adding cart item to database for user: ${props.userId}` + ); + const r = await addCartItemToDB({ + product_id: cartItemToAdd.product_id, + quantity: cartItemToAdd.quantity, + user_id: cartItemToAdd.user_id, + }); + if (r.data) { + cartItemToAdd.id = r.data.id; + } else { + console.error(`AddToCartButton: error adding cart item: ${r.error}`); + } + } + + // add product to cart items + dispatch(addCartItem(cartItemToAdd)); + + // show toast + setToastVisible(true); + }; + + return ( + <> + + + + ); +}; + +export default AddToCartButton; diff --git a/app/components/cart/CartButton.tsx b/app/components/cart/CartButton.tsx new file mode 100644 index 0000000..f3f8874 --- /dev/null +++ b/app/components/cart/CartButton.tsx @@ -0,0 +1,42 @@ +'use client'; +import {Product} from '@/app/lib/plamatio-backend/types'; +import {FC} from 'react'; +import StatefulCartButton from '@/app/components/cart/StatefulCartButton'; +import {useUser} from '@clerk/nextjs'; +import LoadingSpinner from '../ui/loading-spinner'; +import MergeCartItems from './MergeCartItems'; + +type CartButtonProps = { + product: Product; + showLabel?: boolean; + className?: string; + labelClassName?: string; +}; + +export const CartButton: FC = (props) => { + // get user id + const {isLoaded, user} = useUser(); + // if user is not available + return ( + <> + {!isLoaded ? ( + + ) : ( + <> + + {user && } + + )} + + {/* If user signed in, merge cart items if any difference between local cart items & database */} + + ); +}; + +export default CartButton; diff --git a/app/components/cart/CartIcon.tsx b/app/components/cart/CartIcon.tsx new file mode 100644 index 0000000..19747d3 --- /dev/null +++ b/app/components/cart/CartIcon.tsx @@ -0,0 +1,44 @@ +'use client'; +import classNames from 'classnames'; +import {ShoppingBagIcon} from 'lucide-react'; +import {FC, useState} from 'react'; +import {CartWindow} from './CartWindow'; + +type CartIconProps = { + className?: string; + iconSize?: number; + strokeWidth?: number; + iconClassName?: string; +}; + +export const CartIcon: FC = ({ + className, + iconSize, + strokeWidth, + iconClassName, +}) => { + const [displayCartWindow, setDisplayCartWindow] = useState(false); + + return ( + <> +
+ +
+ {displayCartWindow && ( +
+ +
+ )} + + ); +}; + +export default CartIcon; diff --git a/app/components/cart/CartWindow.tsx b/app/components/cart/CartWindow.tsx new file mode 100644 index 0000000..5b77774 --- /dev/null +++ b/app/components/cart/CartWindow.tsx @@ -0,0 +1,122 @@ +'use client'; +import {useGetProductsQuery} from '@/app/lib/api/products-api-slice'; +import Image from 'next/image'; +import {useMemo} from 'react'; +import {LoadingSpinner} from '../ui/loading-spinner'; +import MutateCartButton from './MutateCartButton'; +import Link from 'next/link'; +import {useAppSelector} from '@/app/lib/store/storeHooks'; +import {selectCartItems} from '@/app/lib/store/reducers/cart/cartReducer'; +import {SmileIcon} from 'lucide-react'; +import {CartItem} from '@/app/lib/plamatio-backend/types'; + +export const CartWindow = () => { + // const userId: string | undefined = undefined; + const cartItems = useAppSelector(selectCartItems); + + const productsFetch = useGetProductsQuery(); + + // Log error if any occurs during fetching products + useMemo(() => { + if (productsFetch.isError) { + console.error( + `${Date.now()} CartWindow: Error fetching products for cart items`, + productsFetch.error + ); + } + }, [productsFetch.isError, productsFetch.error]); + + function getProductName(productId: number): string { + const product = productsFetch.data?.data.find( + (item) => item.id === productId + ); + if (product) { + return product.name; + } + throw new Error('Product not found'); + } + + function getProductPrice(productId: number): number { + const product = productsFetch.data?.data.find( + (item) => item.id === productId + ); + if (product) { + return product.price; + } + throw new Error('Product not found'); + } + + function getProductImageURL(cartItem: CartItem, productId: number): string { + console.log(`Product ID: ${productId}`); + console.dir(cartItem); + const product = productsFetch.data?.data.find( + (item) => item.id === productId + ); + if (product) { + return product.imageUrl; + } + throw new Error('Product not found'); + } + + return ( + <> +
+ Cart Items + {productsFetch.isLoading && ( +
+ +
+ )} +
+ {productsFetch.isSuccess && + cartItems?.map((item) => ( +
+ product image +
+ + {getProductName(item.product_id)} + +
+ + ${getProductPrice(item.product_id)} + + {}} + /> +
+
+
+ ))} + {cartItems && cartItems.length <= 0 && ( +
+ You have nothing here yet :{'('} + + We know you will find something you love{' '} + + +
+ )} +
+ +
+ + ); +}; diff --git a/app/components/cart/CheckoutCartItems.tsx b/app/components/cart/CheckoutCartItems.tsx new file mode 100644 index 0000000..c97fc2a --- /dev/null +++ b/app/components/cart/CheckoutCartItems.tsx @@ -0,0 +1,36 @@ +'use client'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; +import {FC} from 'react'; +import {SmileIcon} from 'lucide-react'; +import Link from 'next/link'; +import ProductCheckoutPreview from '../products/ProductCheckoutPreview'; + +type CheckoutCartItemsProps = { + cartItems: CartItem[]; + products: Product[]; +}; + +export const CheckoutCartItems: FC = ({ + cartItems, + products, +}) => { + return ( +
+ {products.map((product) => ( + + ))} + {cartItems && cartItems.length <= 0 && ( +
+ You have nothing here yet :{'('} + + We know you will find something you love{' '} + + + +
+ )} +
+ ); +}; diff --git a/app/components/cart/MergeCartItems.tsx b/app/components/cart/MergeCartItems.tsx new file mode 100644 index 0000000..1b6b07f --- /dev/null +++ b/app/components/cart/MergeCartItems.tsx @@ -0,0 +1,209 @@ +'use client'; +import { + useAddCartItemsMutation, + useGetCartItemsQuery, + useUpdateCartItemMutation, +} from '@/app/lib/api/cart-items-slice'; +import {CartItemAPIStruct, NewCartItem} from '@/app/lib/plamatio-backend/types'; +import { + selectCartItems, + setNewCartItems, +} from '@/app/lib/store/reducers/cart/cartReducer'; +import {useAppDispatch, useAppSelector} from '@/app/lib/store/storeHooks'; +import {CartItem} from '@/app/lib/plamatio-backend/types'; +import {FC, useEffect, useMemo, useRef, useState} from 'react'; + +/* +Merging cart items from local storage and database: +- If local storage has more items than database + - Add all items from database to merged cart. + - Add items from local storage, that are not already in merged cart, to merged cart. Also, add these items to a separate array of items to be added to database. + - Add missing items to database. +- If database has more items than local storage + - Add all items from local storage to merged cart. + - Add items from database, that are not already in merged cart, to merged cart. +- If both have same number of items + - Add all items from database to merged cart. + - Add all items from local storage, that are not already in merged cart, to merged cart. Also, add these items to a separate array of items to be added to database. + - Add missing items to database. + */ +function mergeCartItems( + localCartItems: CartItem[], + dbCartItems: CartItem[], + userId: string +) { + // store merge cart items + const mergedCartItems: CartItem[] = []; + // store items to be added to database + const itemsToAddToDB: NewCartItem[] = []; + // store items to update + const itemsToUpdate: CartItemAPIStruct[] = []; + + // if database has more items than local storage + if (dbCartItems.length > localCartItems.length) { + // add all local storage items to merged cart + mergedCartItems.push(...localCartItems); + // iterate through database items + for (let i = 0; i < dbCartItems.length; i++) { + // if dbCartItems[i] is not in mergedCartItems + if ( + !mergedCartItems.find( + (item) => item.product_id === dbCartItems[i].product_id + ) + ) { + // add dbCartItems[i] to mergedCartItems + mergedCartItems.push(dbCartItems[i]); + } else { + // if dbCartItems[i] is in mergedCartItems, update ID + const index = mergedCartItems.findIndex( + (item) => item.product_id === dbCartItems[i].product_id + ); + // if product found + if (index) { + const q = mergedCartItems[index].quantity; + const l = dbCartItems[i].quantity; + if (q != l) { + itemsToUpdate.push({ + id: dbCartItems[i].id, + product_id: dbCartItems[i].product_id, + quantity: q, + user_id: userId, + }); + } + // update mergedCartItems + mergedCartItems[index] = { + id: dbCartItems[i].id, + product_id: dbCartItems[i].product_id, + quantity: q, + user_id: userId, + }; + } + } + } + + // return merged cart items + return {mergedCartItems, itemsToAddToDB, itemsToUpdate}; + } + + // if local storage have more than or equal items to database + // add all dbCartItems to merged cart + mergedCartItems.push(...dbCartItems); + + // iterate through local storage items + for (let i = 0; i < localCartItems.length; i++) { + // if localCartItems[i] is not in mergedCartItems + if ( + !mergedCartItems.find( + (item) => item.product_id === localCartItems[i].product_id + ) + ) { + // add localCartItems[i] to mergedCartItems + mergedCartItems.push(localCartItems[i]); + // add localCartItems[i] to itemsToAddToDB + itemsToAddToDB.push({ + product_id: localCartItems[i].product_id, + quantity: localCartItems[i].quantity, + user_id: userId, + }); + } + } + + // return merged cart items and items to be added to database + return {mergedCartItems, itemsToAddToDB, itemsToUpdate}; +} + +type MergeCartItemsProps = { + userId: string; +}; + +export const MergeCartItems: FC = ({userId}) => { + // get cart items for the user + const userCartItemsFetch = useGetCartItemsQuery(userId); + // use selector to get cart items from redux store + const localCartItems = useAppSelector(selectCartItems); + // dispatch to set new cart items + const dispatch = useAppDispatch(); + // mutation to add cart items to database + const [addCartItems, addCartItemsRequest] = useAddCartItemsMutation(); + // mutation to update cart items on database + const [updateCartItem, updateCartItemRequest] = useUpdateCartItemMutation(); + // flag to ensure useEffect runs only once + const ranOnce = useRef(false); + // store items to update + const [itemsToUpdate, setItemsToUpdate] = useState([]); + + useEffect(() => { + if ( + !ranOnce.current && + userCartItemsFetch.isSuccess && + userCartItemsFetch.data?.data + ) { + console.log('MergeCartItems: merging cart items'); + ranOnce.current = true; + // merge cart items from local storage and from database + const {mergedCartItems, itemsToAddToDB, itemsToUpdate} = mergeCartItems( + localCartItems, + userCartItemsFetch.data.data, + userId + ); + // set items to update + setItemsToUpdate(itemsToUpdate); + // set new cart items + dispatch(setNewCartItems(mergedCartItems)); + // add missing items to database + if (itemsToAddToDB.length > 0) { + addCartItems({ + data: itemsToAddToDB, + }); + } + } + }, [ + userCartItemsFetch.data, + userCartItemsFetch.isSuccess, + localCartItems, + addCartItems, + dispatch, + userId, + ]); + + // update cart items on database + useMemo(() => { + if (itemsToUpdate.length > 0) { + console.log('MergeCartItems: updating cart items'); + itemsToUpdate.forEach((item) => { + updateCartItem(item); + }); + } + }, [itemsToUpdate, updateCartItem]); + + // log any error in updating cart items on database + useMemo(() => { + if (updateCartItemRequest.isError) { + console.error( + `MergeCartItems: error updating cart items: ${updateCartItemRequest.error}` + ); + } + }, [updateCartItemRequest.isError, updateCartItemRequest.error]); + + // Log any error in fetching user cart items + useMemo(() => { + if (userCartItemsFetch.isError) { + console.error( + `MergeCartItems: error fetching user cart items: ${userCartItemsFetch.error}` + ); + } + }, [userCartItemsFetch.isError, userCartItemsFetch.error]); + + // Log any error in adding cart items to database + useMemo(() => { + if (addCartItemsRequest.isError) { + console.error( + `MergeCartItems: error adding cart items: ${addCartItemsRequest.error}` + ); + } + }, [addCartItemsRequest.isError, addCartItemsRequest.error]); + + return null; +}; + +export default MergeCartItems; diff --git a/app/components/cart/MutateCartButton.tsx b/app/components/cart/MutateCartButton.tsx new file mode 100644 index 0000000..0770307 --- /dev/null +++ b/app/components/cart/MutateCartButton.tsx @@ -0,0 +1,144 @@ +'use client'; +import {MinusCircleIcon, PlusCircleIcon, ShoppingCartIcon} from 'lucide-react'; +import classNames from 'classnames'; +import {FC, useMemo} from 'react'; +import {CartItem} from '@/app/lib/plamatio-backend/types'; +import {useAppDispatch, useAppSelector} from '@/app/lib/store/storeHooks'; +import { + decrementQuantity, + incrementQuantity, + selectAllowCartChanges, +} from '@/app/lib/store/reducers/cart/cartReducer'; +import { + useDeleteCartItemMutation, + useUpdateCartItemMutation, +} from '@/app/lib/api/cart-items-slice'; + +type MutateCartButtonIconsProps = { + plusIconSize?: number; + plusIconStrokeWidth?: number; + minusIconSize?: number; + minusIconStrokeWidth?: number; + cartIconSize?: number; + cartIconStrokeWidth?: number; +}; + +type MutateCartButtonProps = { + cartItem: CartItem; + className?: string; + iconConfig?: MutateCartButtonIconsProps; + setShowMutateCartButton: (state: boolean) => void; +}; + +export const MutateCartButton: FC = ({ + cartItem, + className, + iconConfig, + setShowMutateCartButton, +}) => { + // dispatch cart actions + const dispatch = useAppDispatch(); + // monitor if cart changes are allowed + const allowCartChanges = useAppSelector(selectAllowCartChanges); + // handle mutations to cart items on database + const [updateCartItem, {isError, error}] = useUpdateCartItemMutation(); + // handle removing cart item from database + const [removeCartItem, {isError: isRemoveError, error: removeError}] = + useDeleteCartItemMutation(); + + function incrementProductQuantity() { + // if cart changes allowed + if (allowCartChanges) { + // increment quantity + dispatch(incrementQuantity(cartItem)); + console.dir(cartItem); + // if cart item has a valid user id + if (cartItem.user_id && cartItem.user_id.length > 0) { + console.log(`Updating cart item`); + console.dir(cartItem); + // update cart item in database + updateCartItem({ + id: cartItem.id, + product_id: cartItem.product_id, + user_id: cartItem.user_id, + quantity: cartItem.quantity + 1, + }); + } + } + } + + function decrementProductQuantity() { + // if cart changes allowed + if (allowCartChanges) { + // if quantity is 1, remove the product from cart + if (cartItem.quantity === 1) { + setShowMutateCartButton(false); + } + // if cart item has a valid user id + if (cartItem.user_id && cartItem.user_id.length > 0) { + // if quantity is 1, remove the product from cart + if (cartItem.quantity === 1) { + // remove cart item from database + removeCartItem({cartItemId: cartItem.id, userId: cartItem.user_id}); + } else { + // update cart item in database + updateCartItem({ + id: cartItem.id, + product_id: cartItem.product_id, + user_id: cartItem.user_id, + quantity: cartItem.quantity - 1, + }); + } + } + // decrement quantity (if quantity is 1, will automatically remove the product from cart) + dispatch(decrementQuantity(cartItem)); + } + } + + // Log any error in updating cart item on database + useMemo(() => { + if (isError) { + console.error(`MutateCartButton: error updating cart item: ${error}`); + } + }, [isError, error]); + + // Log any error in removing cart item from database + useMemo(() => { + if (isRemoveError) { + console.error( + `MutateCartButton: error removing cart item: ${removeError}` + ); + } + }, [isRemoveError, removeError]); + + return ( + <> +
+ + {cartItem.quantity} + + +
+ + ); +}; + +export default MutateCartButton; diff --git a/app/components/cart/noUserCartButton.tsx b/app/components/cart/StatefulCartButton.tsx similarity index 80% rename from app/components/cart/noUserCartButton.tsx rename to app/components/cart/StatefulCartButton.tsx index 09c987f..50de681 100644 --- a/app/components/cart/noUserCartButton.tsx +++ b/app/components/cart/StatefulCartButton.tsx @@ -1,20 +1,21 @@ 'use client'; -import {CartItem, Product} from '@/app/types/backend-types'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; import {FC, useEffect, useMemo, useState} from 'react'; -import {MutateCartButton} from './mutateCartButton'; -import {AddToCartButton} from './addToCartButton'; +import {MutateCartButton} from './MutateCartButton'; +import {AddToCartButton} from './AddToCartButton'; import {useAppSelector} from '@/app/lib/store/storeHooks'; import {selectCartItems} from '@/app/lib/store/reducers/cart/cartReducer'; -type NoUserCartButtonProps = { +type StatefulCartButtonProps = { product: Product; + userId?: string; showLabel?: boolean; className?: string; labelClassName?: string; }; -export const NoUserCartButton: FC = (props) => { +export const StatefulCartButton: FC = (props) => { // state to store cart items const [cartItem, setCartItem] = useState(undefined); // use selector to get cart items from redux store @@ -30,9 +31,8 @@ export const NoUserCartButton: FC = (props) => { if (cartItems) { // get the cart item matching the product id or the product provided in props const cartItem = cartItems.find( - (item: CartItem) => item.productId === props.product.id + (item: CartItem) => item.product_id === props.product.id ); - console.log(`NoUserCartButton: cart item: ${cartItem}`); // if valid cart item available, set the cart item if (cartItem) { setCartItem(cartItem); @@ -47,7 +47,7 @@ export const NoUserCartButton: FC = (props) => { if (cartItems) { // get the cart item matching the product id or the product provided in props const cartItem = cartItems.find( - (item: CartItem) => item.productId === props.product.id + (item: CartItem) => item.product_id === props.product.id ); // if valid cart item available, set the cart item if (cartItem) { @@ -66,6 +66,7 @@ export const NoUserCartButton: FC = (props) => { /> ) : ( = (props) => { ); }; -export default NoUserCartButton; +export default StatefulCartButton; diff --git a/app/components/cart/UserCartButton.tsx b/app/components/cart/UserCartButton.tsx new file mode 100644 index 0000000..f3f43b2 --- /dev/null +++ b/app/components/cart/UserCartButton.tsx @@ -0,0 +1,62 @@ +'use client'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; +import {FC, useMemo} from 'react'; +import {MutateCartButton} from './MutateCartButton'; +import {AddToCartButton} from './AddToCartButton'; +import {useGetCartItemsQuery} from '@/app/lib/api/cart-items-slice'; + +type UserCartButtonProps = { + userId: string; + product: Product; + showLabel?: boolean; + className?: string; + labelClassName?: string; +}; + +export const UserCartButton: FC = (props) => { + // get cart items for the user + const cartItemsData = useGetCartItemsQuery(props.userId); + + const getCartButton = useMemo(() => { + // get cart item matching the product id or the product provided in props + if (cartItemsData.isSuccess) { + const cartItems = cartItemsData.data.data; + // check if cart items available + if (cartItems) { + const cartItem = cartItems.find( + (item: CartItem) => item.product_id === props.product.id + ); + // if valid cart item available, show the mutate cart button + if (cartItem) { + return ( + {}} + /> + ); + } + } + } + + // else show the add to cart button + return ( + + ); + }, [ + cartItemsData, + props.product, + props.showLabel, + props.className, + props.labelClassName, + ]); + + return getCartButton; +}; + +export default UserCartButton; diff --git a/app/components/cart/addToCartButton.tsx b/app/components/cart/addToCartButton.tsx index da20e4d..e77e86c 100644 --- a/app/components/cart/addToCartButton.tsx +++ b/app/components/cart/addToCartButton.tsx @@ -1,11 +1,12 @@ 'use client'; import {PlusIcon, ShoppingCartIcon} from 'lucide-react'; import classNames from 'classnames'; -import {FC, useEffect, useState} from 'react'; -import {Toast} from '../toast/toast'; -import {CartItem, Product} from '@/app/types/backend-types'; +import {FC, useEffect, useMemo, useState} from 'react'; +import {Toast} from '../toast/Toast'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; import {useAppDispatch} from '@/app/lib/store/storeHooks'; import {addCartItem} from '@/app/lib/store/reducers/cart/cartReducer'; +import {useAddCartItemMutation} from '@/app/lib/api/cart-items-slice'; type AddToCartButtonProps = { product: Product; @@ -21,6 +22,15 @@ export const AddToCartButton: FC = ( const [toastVisible, setToastVisible] = useState(false); // dispatch cart actions const dispatch = useAppDispatch(); + // when user logged in, need to add cart item to database + const [addCartItemToDB, {isError, error}] = useAddCartItemMutation(); + + // Log any error in updating cart item on database + useMemo(() => { + if (isError) { + console.error(`AddToCartButton: error adding cart item: ${error}`); + } + }, [isError, error]); useEffect(() => { if (toastVisible) { @@ -30,20 +40,35 @@ export const AddToCartButton: FC = ( } }, [toastVisible]); - const handleAddToCart = () => { - // if user id not available - if (!props.userId) { - // prepare cart item - const cartItemToAdd: CartItem = { - id: Math.floor(Math.random() * 10000), - productId: props.product.id, - quantity: 1, - userId: '', // no user id - }; - // add product to cart items - dispatch(addCartItem(cartItemToAdd)); + const handleAddToCart = async () => { + // prepare cart item + const cartItemToAdd: CartItem = { + id: Math.floor(Math.random() * 10000), + product_id: props.product.id, + quantity: 1, + user_id: props.userId && props.userId.length > 0 ? props.userId : '', // no user id if not logged in + }; + + // if valid user id, add cart item to database + if (props.userId && props.userId.length > 0) { + console.log( + `AddToCartButton: adding cart item to database for user: ${props.userId}` + ); + const r = await addCartItemToDB({ + product_id: cartItemToAdd.product_id, + quantity: cartItemToAdd.quantity, + user_id: cartItemToAdd.user_id, + }); + if (r.data) { + cartItemToAdd.id = r.data.id; + } else { + console.error(`AddToCartButton: error adding cart item: ${r.error}`); + } } + // add product to cart items + dispatch(addCartItem(cartItemToAdd)); + // show toast setToastVisible(true); }; diff --git a/app/components/cart/cartButton.tsx b/app/components/cart/cartButton.tsx index c5b021e..f3f8874 100644 --- a/app/components/cart/cartButton.tsx +++ b/app/components/cart/cartButton.tsx @@ -1,7 +1,10 @@ -import {Product} from '@/app/types/backend-types'; +'use client'; +import {Product} from '@/app/lib/plamatio-backend/types'; import {FC} from 'react'; -import {UserCartButton} from './userCartButton'; -import NoUserCartButton from './noUserCartButton'; +import StatefulCartButton from '@/app/components/cart/StatefulCartButton'; +import {useUser} from '@clerk/nextjs'; +import LoadingSpinner from '../ui/loading-spinner'; +import MergeCartItems from './MergeCartItems'; type CartButtonProps = { product: Product; @@ -12,24 +15,27 @@ type CartButtonProps = { export const CartButton: FC = (props) => { // get user id - const userId = undefined; + const {isLoaded, user} = useUser(); // if user is not available - return !userId ? ( - - ) : ( - // if user available - + return ( + <> + {!isLoaded ? ( + + ) : ( + <> + + {user && } + + )} + + {/* If user signed in, merge cart items if any difference between local cart items & database */} + ); }; diff --git a/app/components/cart/cartIcon.tsx b/app/components/cart/cartIcon.tsx index 327821e..19747d3 100644 --- a/app/components/cart/cartIcon.tsx +++ b/app/components/cart/cartIcon.tsx @@ -2,7 +2,7 @@ import classNames from 'classnames'; import {ShoppingBagIcon} from 'lucide-react'; import {FC, useState} from 'react'; -import {CartWindow} from './cartWindow'; +import {CartWindow} from './CartWindow'; type CartIconProps = { className?: string; diff --git a/app/components/cart/cartWindow.tsx b/app/components/cart/cartWindow.tsx index 547b101..5b77774 100644 --- a/app/components/cart/cartWindow.tsx +++ b/app/components/cart/cartWindow.tsx @@ -3,11 +3,12 @@ import {useGetProductsQuery} from '@/app/lib/api/products-api-slice'; import Image from 'next/image'; import {useMemo} from 'react'; import {LoadingSpinner} from '../ui/loading-spinner'; -import MutateCartButton from './mutateCartButton'; +import MutateCartButton from './MutateCartButton'; import Link from 'next/link'; import {useAppSelector} from '@/app/lib/store/storeHooks'; import {selectCartItems} from '@/app/lib/store/reducers/cart/cartReducer'; import {SmileIcon} from 'lucide-react'; +import {CartItem} from '@/app/lib/plamatio-backend/types'; export const CartWindow = () => { // const userId: string | undefined = undefined; @@ -45,7 +46,9 @@ export const CartWindow = () => { throw new Error('Product not found'); } - function getProductImageURL(productId: number): string { + function getProductImageURL(cartItem: CartItem, productId: number): string { + console.log(`Product ID: ${productId}`); + console.dir(cartItem); const product = productsFetch.data?.data.find( (item) => item.id === productId ); @@ -71,7 +74,7 @@ export const CartWindow = () => { key={item.id} className="w-full flex flex-row gap-5 min-w-[350px]"> product image { />
- {getProductName(item.productId)} + {getProductName(item.product_id)}
- ${getProductPrice(item.productId)} + ${getProductPrice(item.product_id)} = ({ const dispatch = useAppDispatch(); // monitor if cart changes are allowed const allowCartChanges = useAppSelector(selectAllowCartChanges); + // handle mutations to cart items on database + const [updateCartItem, {isError, error}] = useUpdateCartItemMutation(); + // handle removing cart item from database + const [removeCartItem, {isError: isRemoveError, error: removeError}] = + useDeleteCartItemMutation(); function incrementProductQuantity() { // if cart changes allowed if (allowCartChanges) { // increment quantity dispatch(incrementQuantity(cartItem)); + console.dir(cartItem); + // if cart item has a valid user id + if (cartItem.user_id && cartItem.user_id.length > 0) { + console.log(`Updating cart item`); + console.dir(cartItem); + // update cart item in database + updateCartItem({ + id: cartItem.id, + product_id: cartItem.product_id, + user_id: cartItem.user_id, + quantity: cartItem.quantity + 1, + }); + } } } @@ -52,11 +74,43 @@ export const MutateCartButton: FC = ({ if (cartItem.quantity === 1) { setShowMutateCartButton(false); } - // decrement quantity + // if cart item has a valid user id + if (cartItem.user_id && cartItem.user_id.length > 0) { + // if quantity is 1, remove the product from cart + if (cartItem.quantity === 1) { + // remove cart item from database + removeCartItem({cartItemId: cartItem.id, userId: cartItem.user_id}); + } else { + // update cart item in database + updateCartItem({ + id: cartItem.id, + product_id: cartItem.product_id, + user_id: cartItem.user_id, + quantity: cartItem.quantity - 1, + }); + } + } + // decrement quantity (if quantity is 1, will automatically remove the product from cart) dispatch(decrementQuantity(cartItem)); } } + // Log any error in updating cart item on database + useMemo(() => { + if (isError) { + console.error(`MutateCartButton: error updating cart item: ${error}`); + } + }, [isError, error]); + + // Log any error in removing cart item from database + useMemo(() => { + if (isRemoveError) { + console.error( + `MutateCartButton: error removing cart item: ${removeError}` + ); + } + }, [isRemoveError, removeError]); + return ( <>
= (props) => { // check if cart items available if (cartItems) { const cartItem = cartItems.find( - (item: CartItem) => item.productId === props.product.id + (item: CartItem) => item.product_id === props.product.id ); // if valid cart item available, show the mutate cart button if (cartItem) { diff --git a/app/components/checkout/AddNewOrder.tsx b/app/components/checkout/AddNewOrder.tsx index e6ac62d..7dfb8bf 100644 --- a/app/components/checkout/AddNewOrder.tsx +++ b/app/components/checkout/AddNewOrder.tsx @@ -3,9 +3,9 @@ import {useAddDetailedOrderMutation} from '@/app/lib/api/orders-slice'; import {NewDetailedOrderItem, NewOrder} from '@/app/lib/plamatio-backend/types'; import {useAppDispatch, useAppSelector} from '@/app/lib/store/storeHooks'; -import {FC, useMemo, useState} from 'react'; +import {FC, useMemo, useRef} from 'react'; import {LoadingSpinner} from '../ui/loading-spinner'; -import ErrorFetchingData from '../error/errorFetchingData'; +import ErrorFetchingData from '../error/ErrorFetchingData'; import { clearCart, selectCartItems, @@ -19,8 +19,8 @@ type AddNewOrderProps = { }; export const AddNewOrder: FC = ({order, items}) => { - // track if order has been submitted - const [orderSubmitted, setOrderSubmitted] = useState(false); + // state to store order submitted status + const orderSubmitted = useRef(false); // dispatch to clear cart const dispatch = useAppDispatch(); // select cart items @@ -31,7 +31,7 @@ export const AddNewOrder: FC = ({order, items}) => { useMemo(() => { if ( - !orderSubmitted && + !orderSubmitted.current && order && items && cartItems && @@ -41,17 +41,22 @@ export const AddNewOrder: FC = ({order, items}) => { // dispatch action to add new order addOrder({order, items}); } - }, [order, items, orderSubmitted, cartItems, addOrder]); + }, [order, items, cartItems, addOrder]); useMemo(() => { - if (isSuccess && !orderSubmitted && cartItems && cartItems.length > 0) { + if ( + isSuccess && + !orderSubmitted.current && + cartItems && + cartItems.length > 0 + ) { console.log(`Order added`); + // set order submitted + orderSubmitted.current = true; // clear cart dispatch(clearCart()); - // set order submitted - setOrderSubmitted(true); } - }, [isSuccess, orderSubmitted, cartItems, dispatch]); + }, [isSuccess, cartItems, dispatch]); // log error if any useMemo(() => { diff --git a/app/components/checkout/AddressesSection.tsx b/app/components/checkout/AddressesSection.tsx index f53edbb..b9a37a4 100644 --- a/app/components/checkout/AddressesSection.tsx +++ b/app/components/checkout/AddressesSection.tsx @@ -1,5 +1,5 @@ 'use client'; -import {Address} from '@/app/types/backend-types'; +import {Address} from '@/app/lib/plamatio-backend/types'; import {FC, useMemo, useState} from 'react'; import NewAddressModal from '../addresses/NewAddressModel'; diff --git a/app/components/checkout/CheckoutPaymentModal.tsx b/app/components/checkout/CheckoutPaymentModal.tsx index 8a5aeba..4e9200f 100644 --- a/app/components/checkout/CheckoutPaymentModal.tsx +++ b/app/components/checkout/CheckoutPaymentModal.tsx @@ -5,12 +5,15 @@ import {ChevronRight, XIcon} from 'lucide-react'; import {CheckoutPaymentProvider} from './CheckoutPaymentProvider'; import {useAppSelector} from '@/app/lib/store/storeHooks'; import {useGetProductsQuery} from '@/app/lib/api/products-api-slice'; -import {Product} from '@/app/types/backend-types'; +import { + Product, + NewDetailedOrderItem, + NewOrder, +} from '@/app/lib/plamatio-backend/types'; import {selectCartItems} from '@/app/lib/store/reducers/cart/cartReducer'; import {createCheckoutSession} from '@/app/lib/stripe/actions'; import {getCartLineItems} from '@/app/lib/stripe/utils'; import {LoadingSpinner} from '../ui/loading-spinner'; -import {NewDetailedOrderItem, NewOrder} from '@/app/lib/plamatio-backend/types'; type CheckoutPaymentModalProps = { label?: string; @@ -47,7 +50,7 @@ const CheckoutPaymentModal: FC = ({ useEffect(() => { if (productsFetch.isSuccess) { const products = productsFetch.data?.data.filter((product) => - cartItems.map((item) => item.productId).includes(product.id) + cartItems.map((item) => item.product_id).includes(product.id) ); if (products) { setProductsInCart(products); @@ -67,7 +70,7 @@ const CheckoutPaymentModal: FC = ({ // prepare data for new order items const orderItems: NewDetailedOrderItem[] = cartItems.map((item) => { return { - product_id: item.productId, + product_id: item.product_id, quantity: item.quantity, }; }); diff --git a/app/components/checkout/OrderSection.tsx b/app/components/checkout/OrderSection.tsx index 875a4fd..7e199e5 100644 --- a/app/components/checkout/OrderSection.tsx +++ b/app/components/checkout/OrderSection.tsx @@ -1,5 +1,5 @@ 'use client'; -import {CartItem, Product} from '@/app/types/backend-types'; +import {CartItem, Product} from '@/app/lib/plamatio-backend/types'; import {FC, useMemo} from 'react'; type OrderSectionProps = { @@ -16,7 +16,7 @@ export const OrderSection: FC = ({ const [orderTotal, numberOfItems, taxes] = useMemo(() => { // calculate order total const orderTotal = cartItems.reduce((acc, item) => { - const product = products.find((p) => p.id === item.productId); + const product = products.find((p) => p.id === item.product_id); return ( Math.round( (product ? acc + product.price * item.quantity : acc) * 100 diff --git a/app/components/checkout/UserDetailsSection.tsx b/app/components/checkout/UserDetailsSection.tsx index adc4c00..b62f3ba 100644 --- a/app/components/checkout/UserDetailsSection.tsx +++ b/app/components/checkout/UserDetailsSection.tsx @@ -1,5 +1,5 @@ 'use client'; -import {User} from '@/app/types/backend-types'; +import {User} from '@/app/lib/plamatio-backend/types'; import {FC} from 'react'; type UserDetailsSectionProps = { diff --git a/app/components/error/ErrorFetchingData.tsx b/app/components/error/ErrorFetchingData.tsx new file mode 100644 index 0000000..adbcaa0 --- /dev/null +++ b/app/components/error/ErrorFetchingData.tsx @@ -0,0 +1,41 @@ +import {FC} from 'react'; + +type ErrorFetchingDataProps = { + message?: string; + refetchMethod?: () => void; + displayDetails?: boolean; + error?: unknown; + children?: React.ReactNode; +}; + +export const ErrorFetchingData: FC = ({ + message, + refetchMethod, + displayDetails, + error, + children, +}) => { + return ( +
+ + {message || + 'Error fetching data. Please ensure you are connected to the internet. If the problem persists, contact support.'} + + {refetchMethod && ( + + )} + {displayDetails && ( +
+ {JSON.stringify(error)} +
+ )} + {children} +
+ ); +}; + +export default ErrorFetchingData; diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx new file mode 100644 index 0000000..9f3276b --- /dev/null +++ b/app/components/header/Header.tsx @@ -0,0 +1,30 @@ +import {Great_Vibes} from 'next/font/google'; +import NavMenu from './nav/NavMenu'; +import Link from 'next/link'; +import {Breadcrumb} from './breadcrumb/Breadcrumb'; +import CartIcon from '../cart/CartIcon'; +import SignInSignUpButtons from '../auth/SigInSignUpButtons'; + +const greatVibes = Great_Vibes({weight: '400', subsets: ['latin']}); + +export const Header = () => { + return ( +
+
+ + Plamatio + + +
+
+ +
+
+ +
+ +
+ ); +}; diff --git a/app/components/header/breadcrumb/Breadcrumb.tsx b/app/components/header/breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000..34ffc59 --- /dev/null +++ b/app/components/header/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,62 @@ +'use client'; +import {usePathname} from 'next/navigation'; +import {FC} from 'react'; +import BreadcrumbCategoryPart from './CategoryPart'; +import BreadcrumbSubCategortPart from './SubCategoryPart'; +import BreadcrumbProductPart from './ProductPart'; + +type BreadcrumbProps = { + hidden?: boolean; + url?: string; +}; + +export const Breadcrumb: FC = ({url, hidden}) => { + // get the current URL path unless a URL is provided + const pathname = usePathname(); + if (!url) { + url = pathname; + } + + const parts = url.split('/'); + const categoryIndex = parts.indexOf('category'); + const subCategoryIndex = parts.indexOf('subcategory'); + const productIndex = parts.indexOf('product'); + + const categoryId = + categoryIndex !== -1 && parts[categoryIndex + 1] + ? parseInt(parts[categoryIndex + 1]) + : null; + const subCategoryId = + subCategoryIndex !== -1 && parts[subCategoryIndex + 1] + ? parseInt(parts[subCategoryIndex + 1]) + : null; + const productId = + productIndex !== -1 && parts[productIndex + 1] + ? parseInt(parts[productIndex + 1]) + : null; + + return ( + <> + {categoryId && ( +
+
+ {categoryId && } + {categoryId && subCategoryId && ( + + )} + {categoryId && subCategoryId && productId && ( + + )} +
+
+ )} + + ); +}; diff --git a/app/components/header/breadcrumb/CategoryPart.tsx b/app/components/header/breadcrumb/CategoryPart.tsx new file mode 100644 index 0000000..0fe4d46 --- /dev/null +++ b/app/components/header/breadcrumb/CategoryPart.tsx @@ -0,0 +1,51 @@ +'use client'; +import {useGetCategoryQuery} from '@/app/lib/api/categories-slice'; +import {HomeIcon} from 'lucide-react'; +import Link from 'next/link'; +import {FC, useMemo} from 'react'; + +type BreadcrumbCategoryPartProps = { + categoryId: number; +}; + +export const BreadcrumbCategoryPart: FC = ({ + categoryId, +}) => { + // fetch data + const categoryFetch = useGetCategoryQuery(categoryId); + + // Method to get the categories part of the breadcrumb + const getCategoriesPart = useMemo(() => { + if (categoryFetch && categoryFetch.data) { + return ( + <> + + + + + {'>'} + + + {categoryFetch.data.name} + + + ); + } + + return null; + }, [categoryFetch, categoryId]); + + // Log error if any occurs during fetching data + useMemo(() => { + if (categoryFetch?.isError) { + console.error(`${Date.now()} Breadcrumb: Error fetching data.`); + } + }, [categoryFetch?.isError]); + + return <>{categoryFetch?.isSuccess && <>{getCategoriesPart}}; +}; + +export default BreadcrumbCategoryPart; diff --git a/app/components/header/breadcrumb/ProductPart.tsx b/app/components/header/breadcrumb/ProductPart.tsx new file mode 100644 index 0000000..a33aa2a --- /dev/null +++ b/app/components/header/breadcrumb/ProductPart.tsx @@ -0,0 +1,51 @@ +'use client'; +import {useGetProductQuery} from '@/app/lib/api/products-api-slice'; +import Link from 'next/link'; +import {FC, useMemo} from 'react'; + +type BreadcrumbProductPartProps = { + categoryId: number; + subCategoryId: number; + productId: number; +}; + +export const BreadcrumbProductPart: FC = ({ + categoryId, + subCategoryId, + productId, +}) => { + // fetch data + const productFetch = useGetProductQuery(productId); + + // Method to get the products part of the breadcrumb + const getProductsPart = useMemo(() => { + if (productFetch && productFetch.data) { + return ( + <> + + {'>'} + + + {productFetch.data.name} + + + ); + } + + return null; + }, [productFetch, categoryId, subCategoryId, productId]); + + // Log error if any occurs during fetching data + useMemo(() => { + if (productFetch?.isError) { + console.error(`${Date.now()} Breadcrumb: Error fetching data.`); + } + }, [productFetch?.isError]); + + return <>{productFetch?.isSuccess && <>{getProductsPart}}; +}; + +export default BreadcrumbProductPart; diff --git a/app/components/header/breadcrumb/SubCategoryPart.tsx b/app/components/header/breadcrumb/SubCategoryPart.tsx new file mode 100644 index 0000000..038752f --- /dev/null +++ b/app/components/header/breadcrumb/SubCategoryPart.tsx @@ -0,0 +1,48 @@ +'use client'; +import {useGetSubCategoryQuery} from '@/app/lib/api/categories-slice'; +import Link from 'next/link'; +import {FC, useMemo} from 'react'; + +type BreadcrumbSubCategortPartProps = { + categoryId: number; + subCategoryId: number; +}; + +export const BreadcrumbSubCategortPart: FC = ({ + categoryId, + subCategoryId, +}) => { + const subCategoryFetch = useGetSubCategoryQuery(subCategoryId); + + // Method to get the subcategories part of the breadcrumb + const getSubCategoriesPart = useMemo(() => { + if (subCategoryFetch && subCategoryFetch.data) { + return ( + <> + + {'>'} + + + {subCategoryFetch.data.name} + + + ); + } + + return null; + }, [subCategoryFetch, categoryId, subCategoryId]); + + // Log error if any occurs during fetching data + useMemo(() => { + if (subCategoryFetch?.isError) { + console.error(`${Date.now()} Breadcrumb: Error fetching data.`); + } + }, [subCategoryFetch?.isError]); + + return <>{subCategoryFetch?.isSuccess && <>{getSubCategoriesPart}}; +}; + +export default BreadcrumbSubCategortPart; diff --git a/app/components/header/breadcrumb/breadcrumb.tsx b/app/components/header/breadcrumb/breadcrumb.tsx index e4223df..34ffc59 100644 --- a/app/components/header/breadcrumb/breadcrumb.tsx +++ b/app/components/header/breadcrumb/breadcrumb.tsx @@ -1,9 +1,9 @@ 'use client'; import {usePathname} from 'next/navigation'; import {FC} from 'react'; -import BreadcrumbCategoryPart from './categoryPart'; -import BreadcrumbSubCategortPart from './subCategoryPart'; -import BreadcrumbProductPart from './productPart'; +import BreadcrumbCategoryPart from './CategoryPart'; +import BreadcrumbSubCategortPart from './SubCategoryPart'; +import BreadcrumbProductPart from './ProductPart'; type BreadcrumbProps = { hidden?: boolean; diff --git a/app/components/header/header.tsx b/app/components/header/header.tsx index 66b8055..9f3276b 100644 --- a/app/components/header/header.tsx +++ b/app/components/header/header.tsx @@ -1,9 +1,9 @@ import {Great_Vibes} from 'next/font/google'; -import NavMenu from './nav/nav-menu'; +import NavMenu from './nav/NavMenu'; import Link from 'next/link'; -import {Breadcrumb} from './breadcrumb/breadcrumb'; -import CartIcon from '../cart/cartIcon'; -import SignInSignUpButtons from '../auth/sigInSignUpButtons'; +import {Breadcrumb} from './breadcrumb/Breadcrumb'; +import CartIcon from '../cart/CartIcon'; +import SignInSignUpButtons from '../auth/SigInSignUpButtons'; const greatVibes = Great_Vibes({weight: '400', subsets: ['latin']}); diff --git a/app/components/header/nav/list-item.tsx b/app/components/header/nav/ListItem.tsx similarity index 100% rename from app/components/header/nav/list-item.tsx rename to app/components/header/nav/ListItem.tsx diff --git a/app/components/header/nav/nav-menu.tsx b/app/components/header/nav/NavMenu.tsx similarity index 98% rename from app/components/header/nav/nav-menu.tsx rename to app/components/header/nav/NavMenu.tsx index b371ab1..6353c2f 100644 --- a/app/components/header/nav/nav-menu.tsx +++ b/app/components/header/nav/NavMenu.tsx @@ -1,6 +1,6 @@ import React from 'react'; import * as NavigationMenu from '@radix-ui/react-navigation-menu'; -import {NavMenuItem, NavMenuSubItem} from './nav-menu-item'; +import {NavMenuItem, NavMenuSubItem} from './NavMenuItem'; import { CategoriesCollection, SubCategoriesCollection, diff --git a/app/components/header/nav/nav-menu-item.tsx b/app/components/header/nav/NavMenuItem.tsx similarity index 98% rename from app/components/header/nav/nav-menu-item.tsx rename to app/components/header/nav/NavMenuItem.tsx index 540172c..c196d4b 100644 --- a/app/components/header/nav/nav-menu-item.tsx +++ b/app/components/header/nav/NavMenuItem.tsx @@ -1,6 +1,6 @@ import * as NavigationMenu from '@radix-ui/react-navigation-menu'; import {ChevronDown} from 'lucide-react'; -import NavListItem from './list-item'; +import NavListItem from './ListItem'; import Link from 'next/link'; import {FC} from 'react'; diff --git a/app/components/landing/CreateUser.tsx b/app/components/landing/CreateUser.tsx index ae06fb6..c421fc9 100644 --- a/app/components/landing/CreateUser.tsx +++ b/app/components/landing/CreateUser.tsx @@ -1,6 +1,6 @@ import {useAddCartItemMutation} from '@/app/lib/api/cart-items-slice'; import {useAddUserMutation, useGetUserQuery} from '@/app/lib/api/users-slice'; -import {User} from '@/app/types/backend-types'; +import {User} from '@/app/lib/plamatio-backend/types'; import {FetchBaseQueryError} from '@reduxjs/toolkit/query'; import {FC, useMemo} from 'react'; @@ -68,8 +68,8 @@ export const CreateUser: FC = ({user}) => { console.log('CreateUser: Adding cart item', cartItem); // add cart item with user id addCartItem({ - userId: user.id, - productId: cartItem.productId, + user_id: user.id, + product_id: cartItem.productId, quantity: cartItem.quantity, }); } diff --git a/app/components/landing/Showcase.tsx b/app/components/landing/Showcase.tsx new file mode 100644 index 0000000..72d657d --- /dev/null +++ b/app/components/landing/Showcase.tsx @@ -0,0 +1,24 @@ +import {fetchHeroProducts} from '@/app/lib/plamatio-backend/actions'; +import ProductsShowcase from '../products/ProductsShowcase'; +import {ProductsCollection} from '@/app/lib/plamatio-backend/types'; + +export default async function LandingPageShowcase() { + // Fetch hero products + const heroProductsFetch = await fetchHeroProducts(); + + // Check for error + if (!heroProductsFetch.ok) { + throw new Error(` + Failed to fetch hero products: ${heroProductsFetch.statusText} + `); + } + + // Parse the response + const heroProducts = (await heroProductsFetch.json()) as ProductsCollection; + + return ( + <> + + + ); +} diff --git a/app/components/landing/showcase.tsx b/app/components/landing/showcase.tsx index 302e1ea..72d657d 100644 --- a/app/components/landing/showcase.tsx +++ b/app/components/landing/showcase.tsx @@ -1,5 +1,5 @@ import {fetchHeroProducts} from '@/app/lib/plamatio-backend/actions'; -import ProductsShowcase from '../products/productsShowcase'; +import ProductsShowcase from '../products/ProductsShowcase'; import {ProductsCollection} from '@/app/lib/plamatio-backend/types'; export default async function LandingPageShowcase() { diff --git a/app/components/orders/DetailedOrdersView.tsx b/app/components/orders/DetailedOrdersView.tsx index 0772e8d..cfbcfd8 100644 --- a/app/components/orders/DetailedOrdersView.tsx +++ b/app/components/orders/DetailedOrdersView.tsx @@ -5,7 +5,7 @@ import {Raleway} from 'next/font/google'; import Link from 'next/link'; import {RabbitIcon} from 'lucide-react'; import {useGetDetailedOrdersQuery} from '@/app/lib/api/orders-slice'; -import ErrorFetchingData from '../error/errorFetchingData'; +import ErrorFetchingData from '../error/ErrorFetchingData'; import {LoadingSpinner} from '../ui/loading-spinner'; const raleway = Raleway({weight: '500', subsets: ['latin']}); diff --git a/app/components/orders/OrderAddress.tsx b/app/components/orders/OrderAddress.tsx index d0d30b1..5b1edac 100644 --- a/app/components/orders/OrderAddress.tsx +++ b/app/components/orders/OrderAddress.tsx @@ -2,7 +2,7 @@ import {useGetAddressQuery} from '@/app/lib/api/users-slice'; import {FC, useMemo} from 'react'; import {LoadingSpinner} from '../ui/loading-spinner'; -import ErrorFetchingData from '../error/errorFetchingData'; +import ErrorFetchingData from '../error/ErrorFetchingData'; type OrderAddressProps = { addressId: number; diff --git a/app/components/orders/OrderDetailsTile.tsx b/app/components/orders/OrderDetailsTile.tsx index f830760..6e2b8b5 100644 --- a/app/components/orders/OrderDetailsTile.tsx +++ b/app/components/orders/OrderDetailsTile.tsx @@ -1,11 +1,13 @@ 'use client'; -import {DetailedOrder} from '@/app/types/backend-types'; +import { + DetailedOrder, + DetailedOrderAPIResponse, +} from '@/app/lib/plamatio-backend/types'; import {OrderAddress} from './OrderAddress'; import {OrderItemTile} from './OrderItemTile'; import {FC, useMemo} from 'react'; import {BadgeCheckIcon, CarIcon, RabbitIcon} from 'lucide-react'; import {formatAmountForDisplay} from '@/app/lib/stripe/utils'; -import {DetailedOrderAPIResponse} from '@/app/lib/plamatio-backend/types'; function getDetailedOrder(response: DetailedOrderAPIResponse): DetailedOrder { return { diff --git a/app/components/orders/OrderItemTile.tsx b/app/components/orders/OrderItemTile.tsx index 62c23d1..7190d28 100644 --- a/app/components/orders/OrderItemTile.tsx +++ b/app/components/orders/OrderItemTile.tsx @@ -1,11 +1,11 @@ 'use client'; import {useGetProductQuery} from '@/app/lib/api/products-api-slice'; -import {OrderItem} from '@/app/types/backend-types'; +import {OrderItem} from '@/app/lib/plamatio-backend/types'; import Image from 'next/image'; import Link from 'next/link'; import {FC, useMemo} from 'react'; import {LoadingSpinner} from '../ui/loading-spinner'; -import ErrorFetchingData from '../error/errorFetchingData'; +import ErrorFetchingData from '../error/ErrorFetchingData'; type OrderItemTileProps = { orderItem: OrderItem; diff --git a/app/components/products/ProductCheckoutPreview.tsx b/app/components/products/ProductCheckoutPreview.tsx new file mode 100644 index 0000000..efe7321 --- /dev/null +++ b/app/components/products/ProductCheckoutPreview.tsx @@ -0,0 +1,64 @@ +import {Product} from '@/app/lib/plamatio-backend/types'; +import Image from 'next/image'; +import {FC} from 'react'; +import Link from 'next/link'; +import classNames from 'classnames'; +import CartButton from '../cart/CartButton'; + +type ProductPreviewProps = { + product: Product; + className?: string; + labelClassName?: string; +}; + +export const ProductCheckoutPreview: FC = ({ + product, + className, + labelClassName, +}) => { + return ( +
+ {product.name} +
+
+ + {product.name} + + {product.description} +
+
+
+ ${product.price} + {product.previousPrice && ( + ${product.previousPrice} + )} +
+ +
+ + View more products like this. + +
+
+ ); +}; + +export default ProductCheckoutPreview; diff --git a/app/components/products/ProductPreview.tsx b/app/components/products/ProductPreview.tsx new file mode 100644 index 0000000..e894aa7 --- /dev/null +++ b/app/components/products/ProductPreview.tsx @@ -0,0 +1,117 @@ +import {Product} from '@/app/lib/plamatio-backend/types'; +import Image from 'next/image'; +import {FC} from 'react'; +import Link from 'next/link'; +import classNames from 'classnames'; +import CartButton from '../cart/CartButton'; + +type ProductStyleConfig = { + nameClassName?: string; + descriptionClassName?: string; + priceClassName?: string; + previousPriceClass?: string; + addToCartButtonClassName?: string; + addToCartButtonLabelClassName?: string; + viewMoreClassName?: string; + detailsContainerClassName?: string; + priceContainerClassName?: string; + priceAddToCartContainerClassName?: string; +}; + +type ProductImageStyleConfig = { + width?: number; + minWidth?: number; + maxWidth?: number; + maxWidthMD?: number; + height?: number; + className?: string; +}; + +type ProductPreviewProps = { + product: Product; + className?: string; + imageStyleConfig?: ProductImageStyleConfig; + productStyleConfig?: ProductStyleConfig; +}; + +export const ProductPreview: FC = ({ + product, + className, + imageStyleConfig, + productStyleConfig, +}) => { + return ( +
+
+ {product.name} +
+
+
+ + {product.name} + + {product.description} +
+
+
+ + ${product.price} + + {product.previousPrice && ( + + ${product.previousPrice} + + )} +
+ +
+ + View more products like this. + +
+
+ ); +}; diff --git a/app/components/products/ProductTile.tsx b/app/components/products/ProductTile.tsx new file mode 100644 index 0000000..2d8bf8c --- /dev/null +++ b/app/components/products/ProductTile.tsx @@ -0,0 +1,59 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import classNames from 'classnames'; +import {Product} from '@/app/lib/plamatio-backend/types'; +import CartButton from '../cart/CartButton'; + +type ProductTileProps = { + product: Product; + numberOfProducts: number; + className?: string; +}; + +export const ProductTile = ({ + product, + numberOfProducts, + className, +}: ProductTileProps) => { + const tileWidth = numberOfProducts === 3 ? 'md:w-[350px]' : 'w-[330px]'; + return ( +
+ + {product.name} + +
+
+ {product.name} + {product.description} +
+ +
+
+ ${product.price} + {product.previousPrice && + product.price !== product.previousPrice && ( + + ${product.previousPrice} + + )} +
+ +
+
+
+ ); +}; diff --git a/app/components/products/ProductsShowcase.tsx b/app/components/products/ProductsShowcase.tsx new file mode 100644 index 0000000..78466e4 --- /dev/null +++ b/app/components/products/ProductsShowcase.tsx @@ -0,0 +1,42 @@ +import {Product} from '@/app/lib/plamatio-backend/types'; +import {ProductTile} from './ProductTile'; +import classNames from 'classnames'; +import {FC} from 'react'; +import ProductsShowcaseWithSubcategories from './ProductsShowcaseWithSubcategories'; + +type ProductsShowcaseProps = { + products: Product[]; + className?: string; + tileClassName?: string; + showSubcategories?: boolean; + categoryId?: number; +}; + +export const ProductsShowcase: FC = ( + props: ProductsShowcaseProps +) => { + return ( + <> + {!props.showSubcategories ? ( +
+ {props.products.map((product) => ( + + ))} +
+ ) : ( + + )} + + ); +}; + +export default ProductsShowcase; diff --git a/app/components/products/ProductsShowcaseWithSubcategories.tsx b/app/components/products/ProductsShowcaseWithSubcategories.tsx new file mode 100644 index 0000000..ec9edea --- /dev/null +++ b/app/components/products/ProductsShowcaseWithSubcategories.tsx @@ -0,0 +1,96 @@ +import {Product} from '@/app/lib/plamatio-backend/types'; +import {ProductTile} from './ProductTile'; +import classNames from 'classnames'; +import {FC, useMemo} from 'react'; +import Link from 'next/link'; +import {Gayathri} from 'next/font/google'; +import {useGetSubCategoriesByCategoryQuery} from '@/app/lib/api/categories-slice'; +import {LoadingSpinner} from '../ui/loading-spinner'; +import ErrorFetchingData from '../error/ErrorFetchingData'; + +const gayathri = Gayathri({weight: '400', subsets: ['latin']}); + +type ProductsShowcaseProps = { + products: Product[]; + className?: string; + tileClassName?: string; + categoryId?: number; +}; + +export const ProductsShowcaseWithSubcategories: FC = ( + props: ProductsShowcaseProps +) => { + const categoryId = props.categoryId; + + // if showing subcategories + if (!categoryId || categoryId <= 0) { + console.error( + 'ProductsShowcaseWithSubcategories: categoryId is required. Will show products without subcategories by default.' + ); + } + + // get data for the subcategories + const subCategories = useGetSubCategoriesByCategoryQuery(categoryId || -1); + + // Log error if any occurs during fetching data + useMemo(() => { + if (subCategories && subCategories.isError) { + console.error( + `${Date.now()} CategoryPage: Error fetching subcategories for category ${props.categoryId}`, + subCategories.error + ); + } + }, [subCategories, props.categoryId]); + + const getSubCategoryName = (subCategoryId: number) => { + let name = ''; + if (subCategories?.isSuccess) { + const subCategory = subCategories.data.data.find( + (subCategory) => subCategory.id === subCategoryId + ); + name = subCategory?.name || ''; + } + return name; + }; + + return ( + <> + {subCategories?.isLoading && ( +
+ +
+ )} + {subCategories?.isError && ( + + )} + {subCategories && subCategories.isSuccess && ( +
+ {props.products.map((product) => ( +
+ + + Shop {getSubCategoryName(product.subCategory) || 'All'} + + + +
+ ))} +
+ )} + + ); +}; + +export default ProductsShowcaseWithSubcategories; diff --git a/app/components/products/productCheckoutPreview.tsx b/app/components/products/productCheckoutPreview.tsx index 934a91a..efe7321 100644 --- a/app/components/products/productCheckoutPreview.tsx +++ b/app/components/products/productCheckoutPreview.tsx @@ -1,9 +1,9 @@ -import {Product} from '@/app/types/backend-types'; +import {Product} from '@/app/lib/plamatio-backend/types'; import Image from 'next/image'; import {FC} from 'react'; import Link from 'next/link'; import classNames from 'classnames'; -import CartButton from '../cart/cartButton'; +import CartButton from '../cart/CartButton'; type ProductPreviewProps = { product: Product; diff --git a/app/components/products/productPreview.tsx b/app/components/products/productPreview.tsx index b53fc43..e894aa7 100644 --- a/app/components/products/productPreview.tsx +++ b/app/components/products/productPreview.tsx @@ -1,9 +1,9 @@ -import {Product} from '@/app/types/backend-types'; +import {Product} from '@/app/lib/plamatio-backend/types'; import Image from 'next/image'; import {FC} from 'react'; import Link from 'next/link'; import classNames from 'classnames'; -import CartButton from '../cart/cartButton'; +import CartButton from '../cart/CartButton'; type ProductStyleConfig = { nameClassName?: string; diff --git a/app/components/products/productTile.tsx b/app/components/products/productTile.tsx index dffbae8..2d8bf8c 100644 --- a/app/components/products/productTile.tsx +++ b/app/components/products/productTile.tsx @@ -1,8 +1,8 @@ import Image from 'next/image'; import Link from 'next/link'; import classNames from 'classnames'; -import {Product} from '@/app/types/backend-types'; -import CartButton from '../cart/cartButton'; +import {Product} from '@/app/lib/plamatio-backend/types'; +import CartButton from '../cart/CartButton'; type ProductTileProps = { product: Product; diff --git a/app/components/products/productsShowcase.tsx b/app/components/products/productsShowcase.tsx index 26d2d20..78466e4 100644 --- a/app/components/products/productsShowcase.tsx +++ b/app/components/products/productsShowcase.tsx @@ -1,8 +1,8 @@ -import {Product} from '@/app/types/backend-types'; -import {ProductTile} from '../products/productTile'; +import {Product} from '@/app/lib/plamatio-backend/types'; +import {ProductTile} from './ProductTile'; import classNames from 'classnames'; import {FC} from 'react'; -import ProductsShowcaseWithSubcategories from './productsShowcaseWithSubcategories'; +import ProductsShowcaseWithSubcategories from './ProductsShowcaseWithSubcategories'; type ProductsShowcaseProps = { products: Product[]; diff --git a/app/components/products/productsShowcaseWithSubcategories.tsx b/app/components/products/productsShowcaseWithSubcategories.tsx index 3afede4..ec9edea 100644 --- a/app/components/products/productsShowcaseWithSubcategories.tsx +++ b/app/components/products/productsShowcaseWithSubcategories.tsx @@ -1,12 +1,12 @@ -import {Product} from '@/app/types/backend-types'; -import {ProductTile} from '../products/productTile'; +import {Product} from '@/app/lib/plamatio-backend/types'; +import {ProductTile} from './ProductTile'; import classNames from 'classnames'; import {FC, useMemo} from 'react'; import Link from 'next/link'; import {Gayathri} from 'next/font/google'; import {useGetSubCategoriesByCategoryQuery} from '@/app/lib/api/categories-slice'; import {LoadingSpinner} from '../ui/loading-spinner'; -import ErrorFetchingData from '../error/errorFetchingData'; +import ErrorFetchingData from '../error/ErrorFetchingData'; const gayathri = Gayathri({weight: '400', subsets: ['latin']}); diff --git a/app/components/toast/Toast.tsx b/app/components/toast/Toast.tsx new file mode 100644 index 0000000..5ae3f67 --- /dev/null +++ b/app/components/toast/Toast.tsx @@ -0,0 +1,32 @@ +import {HeartHandshakeIcon, XIcon} from 'lucide-react'; +import {FC} from 'react'; + +type ToastProps = { + title: string; + visible: boolean; + setVisible: (visible: boolean) => void; + description?: string; +}; + +export const Toast: FC = (props: ToastProps) => { + return ( + + ); +}; diff --git a/app/components/ui/loading-spinner.tsx b/app/components/ui/loading-spinner.tsx index 8f91737..43ed1e6 100644 --- a/app/components/ui/loading-spinner.tsx +++ b/app/components/ui/loading-spinner.tsx @@ -65,6 +65,8 @@ const DefaultLoadingSpinner = ({ ); }; +export default LoadingSpinner; + const ButtonStyleLoadingSpinner = ({ label, h, diff --git a/app/layout.tsx b/app/layout.tsx index d393001..0326f00 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,7 @@ import type {Metadata} from 'next'; import {Work_Sans} from 'next/font/google'; import './globals.css'; import {ClerkProvider} from '@clerk/nextjs'; -import {Header} from './components/header/header'; +import {Header} from './components/header/Header'; import {ReduxProvider} from './ReduxProvider'; import LoadCartItems from './components/auxiliary/LoadCartItems'; import LoadProducts from './components/auxiliary/LoadProducts'; diff --git a/app/lib/api/api-slice.ts b/app/lib/api/api-slice.ts index 6bda081..f28b2f4 100644 --- a/app/lib/api/api-slice.ts +++ b/app/lib/api/api-slice.ts @@ -1,6 +1,6 @@ // Import the RTK Query methods from the React-specific entry point import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react'; -import {getPlamatioBackendAPIURL} from '../plamatio-backend/plamatio-api'; +import {getPlamatioBackendAPIURL} from '../plamatio-backend/utils'; export const apiSlice = createApi({ reducerPath: 'api', diff --git a/app/lib/api/cart-items-slice.ts b/app/lib/api/cart-items-slice.ts index 383f18b..a4540bb 100644 --- a/app/lib/api/cart-items-slice.ts +++ b/app/lib/api/cart-items-slice.ts @@ -1,10 +1,14 @@ -import {CartItem, NewCartItem} from '@/app/types/backend-types'; import {apiSlice} from './api-slice'; +import {PLAMATIO_BACKEND_ENDPOINTS as PBE} from '@/app/lib/plamatio-backend/endpoints'; import { - getPlamatioBackendAPIKey, - PLAMATIO_BACKEND_ENDPOINTS as PBE, -} from '../plamatio-backend/plamatio-api'; -import {CartItemsCollection} from '../plamatio-backend/types'; + CartItem, + CartItemAPIStruct, + CartItemDeleteParams, + CartItemsCollection, + NewCartItem, + NewCartItemsCollection, +} from '@/app/lib/plamatio-backend/types'; +import {getPlamatioBackendAPIKey} from '../plamatio-backend/utils'; export const cartItemsApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ @@ -17,7 +21,7 @@ export const cartItemsApiSlice = apiSlice.injectEndpoints({ }, }), providesTags: (result) => { - if (result) { + if (result && result.data) { return [ 'CartItems', ...result.data.map(({id}) => ({type: 'CartItem', id}) as const), @@ -50,7 +54,20 @@ export const cartItemsApiSlice = apiSlice.injectEndpoints({ return result ? [{type: 'CartItem', id: result.id}] : ['CartItems']; }, }), - updateCartItem: builder.mutation({ + addCartItems: builder.mutation( + { + query: (newCartItems) => ({ + url: PBE.CART.ADD_ALL(), + method: 'POST', + body: newCartItems, + headers: { + Authorization: `Bearer ${getPlamatioBackendAPIKey()}`, + }, + }), + invalidatesTags: ['CartItems'], + } + ), + updateCartItem: builder.mutation({ query: (updatedCart) => ({ url: PBE.CART.UPDATE(), method: 'PUT', @@ -63,16 +80,16 @@ export const cartItemsApiSlice = apiSlice.injectEndpoints({ return result ? [{type: 'CartItem', id: args.id}] : ['CartItems']; }, }), - deleteCartItem: builder.mutation({ - query: (cartItemId) => ({ - url: PBE.CART.DELETE(cartItemId), + deleteCartItem: builder.mutation({ + query: (cartItemDeleteParams) => ({ + url: PBE.CART.DELETE(cartItemDeleteParams), method: 'DELETE', headers: { Authorization: `Bearer ${getPlamatioBackendAPIKey()}`, }, }), - invalidatesTags: (result, _, args) => { - return result ? [{type: 'CartItem', id: args}] : ['CartItems']; + invalidatesTags: (result) => { + return result ? [{type: 'CartItem', id: result}] : ['CartItems']; }, }), }), @@ -82,6 +99,7 @@ export const { useGetCartItemsQuery, useGetCartItemQuery, useAddCartItemMutation, + useAddCartItemsMutation, useUpdateCartItemMutation, useDeleteCartItemMutation, } = cartItemsApiSlice; diff --git a/app/lib/api/categories-slice.ts b/app/lib/api/categories-slice.ts index 664d9fa..607f74f 100644 --- a/app/lib/api/categories-slice.ts +++ b/app/lib/api/categories-slice.ts @@ -1,13 +1,12 @@ import {apiSlice} from './api-slice'; +import {PLAMATIO_BACKEND_ENDPOINTS as PBE} from '../plamatio-backend/endpoints'; import { - getPlamatioBackendAPIKey, - PLAMATIO_BACKEND_ENDPOINTS as PBE, -} from '../plamatio-backend/plamatio-api'; -import { + Category, + SubCategory, CategoriesCollection, SubCategoriesCollection, -} from '../plamatio-backend/types'; -import {Category, SubCategory} from '@/app/types/backend-types'; +} from '@/app/lib/plamatio-backend/types'; +import {getPlamatioBackendAPIKey} from '../plamatio-backend/utils'; export const productsApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ diff --git a/app/lib/api/orders-slice.ts b/app/lib/api/orders-slice.ts index 0e7212e..f368eb8 100644 --- a/app/lib/api/orders-slice.ts +++ b/app/lib/api/orders-slice.ts @@ -1,14 +1,13 @@ -import {DetailedOrder, Order} from '@/app/types/backend-types'; -import {apiSlice} from './api-slice'; -import { - getPlamatioBackendAPIKey, - PLAMATIO_BACKEND_ENDPOINTS as PBE, -} from '../plamatio-backend/plamatio-api'; import { + DetailedOrder, + Order, DetailedOrdersCollection, NewDetailedOrder, OrdersCollection, -} from '../plamatio-backend/types'; +} from '@/app/lib/plamatio-backend/types'; +import {apiSlice} from './api-slice'; +import {PLAMATIO_BACKEND_ENDPOINTS as PBE} from '@/app/lib/plamatio-backend/endpoints'; +import {getPlamatioBackendAPIKey} from '@/app/lib/plamatio-backend/utils'; export const ordersApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ diff --git a/app/lib/api/products-api-slice.ts b/app/lib/api/products-api-slice.ts index ea82c97..5772a34 100644 --- a/app/lib/api/products-api-slice.ts +++ b/app/lib/api/products-api-slice.ts @@ -1,10 +1,7 @@ -import {Product} from '@/app/types/backend-types'; +import {Product, ProductsCollection} from '@/app/lib/plamatio-backend/types'; import {apiSlice} from './api-slice'; -import { - getPlamatioBackendAPIKey, - PLAMATIO_BACKEND_ENDPOINTS as PBE, -} from '../plamatio-backend/plamatio-api'; -import {ProductsCollection} from '../plamatio-backend/types'; +import {PLAMATIO_BACKEND_ENDPOINTS as PBE} from '@/app/lib/plamatio-backend/endpoints'; +import {getPlamatioBackendAPIKey} from '@/app/lib/plamatio-backend/utils'; export const productsApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ diff --git a/app/lib/api/users-slice.ts b/app/lib/api/users-slice.ts index e3e758a..74ef05d 100644 --- a/app/lib/api/users-slice.ts +++ b/app/lib/api/users-slice.ts @@ -1,13 +1,13 @@ -import {Address, NewAddress, User} from '@/app/types/backend-types'; -import {apiSlice} from './api-slice'; -import { - getPlamatioBackendAPIKey, - PLAMATIO_BACKEND_ENDPOINTS as PBE, -} from '../plamatio-backend/plamatio-api'; import { + Address, + NewAddress, + User, AddressesCollection, - DeleteAddressRequestParams, -} from '../plamatio-backend/types'; + DeleteAddressParams, +} from '@/app/lib/plamatio-backend/types'; +import {apiSlice} from './api-slice'; +import {PLAMATIO_BACKEND_ENDPOINTS as PBE} from '@/app/lib/plamatio-backend/endpoints'; +import {getPlamatioBackendAPIKey} from '@/app/lib/plamatio-backend/utils'; export const usersApiSlice = apiSlice.injectEndpoints({ endpoints: (builder) => ({ @@ -101,7 +101,7 @@ export const usersApiSlice = apiSlice.injectEndpoints({ }), invalidatesTags: ['Addresses'], }), - deleteUserAddress: builder.mutation({ + deleteUserAddress: builder.mutation({ query: (deleteAddressRequestParams) => ({ url: PBE.USERS.DELETE_ADDRESS(deleteAddressRequestParams), method: 'DELETE', diff --git a/app/lib/hooks/useCartHooks.ts b/app/lib/hooks/useCartHooks.ts deleted file mode 100644 index 20aa30e..0000000 --- a/app/lib/hooks/useCartHooks.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {CartItemsCollection} from '../plamatio-backend/types'; - -export const useCartHooks = () => { - function getCartItems(userId: string): CartItemsCollection { - console.log(userId); - return {data: []}; - } - - return { - getCartItems, - }; -}; - -export default useCartHooks; diff --git a/app/lib/plamatio-backend/actions.ts b/app/lib/plamatio-backend/actions.ts index 7678ede..ad6fc4d 100644 --- a/app/lib/plamatio-backend/actions.ts +++ b/app/lib/plamatio-backend/actions.ts @@ -1,8 +1,5 @@ -import { - getPlamatioBackendAPIKey, - getPlamatioBackendAPIURL, - PLAMATIO_BACKEND_ENDPOINTS as ENDPOINTS, -} from './plamatio-api'; +import {PLAMATIO_BACKEND_ENDPOINTS as ENDPOINTS} from './endpoints'; +import {getPlamatioBackendAPIKey, getPlamatioBackendAPIURL} from './utils'; export const getRequestOptions = () => ({ headers: { diff --git a/app/lib/plamatio-backend/plamatio-api.ts b/app/lib/plamatio-backend/endpoints.ts similarity index 78% rename from app/lib/plamatio-backend/plamatio-api.ts rename to app/lib/plamatio-backend/endpoints.ts index 8de2dc5..12ac6a6 100644 --- a/app/lib/plamatio-backend/plamatio-api.ts +++ b/app/lib/plamatio-backend/endpoints.ts @@ -1,26 +1,7 @@ -import {DeleteAddressRequestParams} from './types'; - -/** - * Plamatio Backend API URL - */ -export const getPlamatioBackendAPIURL = () => { - const url = process.env.NEXT_PUBLIC_PLAMATIO_BACKEND_API_URL; - if (!url) { - throw new Error('Plamatio Backend API URL not found'); - } - return url; -}; - -/** - * Plamatio Backend API Key - */ -export const getPlamatioBackendAPIKey = () => { - const apiKey = process.env.NEXT_PUBLIC_PLAMATIO_BACKEND_API_KEY; - if (!apiKey) { - throw new Error('Plamatio Backend API Key not found'); - } - return apiKey; -}; +import type { + CartItemDeleteParams, + DeleteAddressParams, +} from '@/app/lib/plamatio-backend/types'; /** * Plamatio Backend Endpoints @@ -52,8 +33,10 @@ export const PLAMATIO_BACKEND_ENDPOINTS = { GET: (id: number) => `/cart/get/${id}`, GET_ALL: (userId: string) => `/cart/all/${userId}`, ADD: () => '/cart/add', + ADD_ALL: () => '/cart/add/all', UPDATE: () => '/cart/update', - DELETE: (id: number) => `/cart/delete/${id}`, + DELETE: (params: CartItemDeleteParams) => + `/cart/delete/${params.cartItemId}/user/${params.userId}`, }, USERS: { GET: (id: string) => `/users/get/${id}`, @@ -64,7 +47,7 @@ export const PLAMATIO_BACKEND_ENDPOINTS = { UPDATE_USER: () => '/users/update', UPDATE_ADDRESS: () => '/users/addresses/update', DELETE: (id: string) => `/users/delete/${id}`, - DELETE_ADDRESS: (params: DeleteAddressRequestParams) => + DELETE_ADDRESS: (params: DeleteAddressParams) => `/users/addresses/delete/${params.addressId}/user/${params.userId}`, }, ORDERS: { diff --git a/app/lib/plamatio-backend/types.ts b/app/lib/plamatio-backend/types.ts index 958a392..9d3db10 100644 --- a/app/lib/plamatio-backend/types.ts +++ b/app/lib/plamatio-backend/types.ts @@ -1,75 +1,43 @@ -import { +export type { + User, Address, - CartItem, - Category, - Order, - Product, - SubCategory, -} from '@/app/types/backend-types'; - -export type ProductsCollection = { - data: Product[]; -}; - -export type CategoriesCollection = { - data: Category[]; -}; - -export type SubCategoriesCollection = { - data: SubCategory[]; -}; - -export type CartItemsCollection = { - data: CartItem[]; -}; - -export type AddressesCollection = { - data: Address[]; -}; + AddressesCollection, + NewUser, + NewAddress, + DeleteAddressParams, +} from './types/user-types'; -export type DeleteAddressRequestParams = { - addressId: number; - userId: string; -}; - -export type OrdersCollection = { - data: Order[]; -}; - -export type DetailedOrderAPIResponse = { - order: { - id: number; - user_id: string; - address_id: number; - total_price: number; - created_at: string; - status: string; - }; - items: { - id: number; - order_id: number; - product_id: number; - quantity: number; - }[]; -}; - -export type DetailedOrdersCollection = { - data: DetailedOrderAPIResponse[]; -}; - -export type NewOrder = { - user_id: string; - address_id: number; - total_price: number; - status: string; -}; +export type { + Product, + CategoryHeroProduct, + ProductsCollection, +} from './types/product-types'; -export type NewDetailedOrderItem = { - product_id: number; - quantity: number; -}; +export type { + Category, + SubCategory, + CategoriesCollection, + SubCategoriesCollection, +} from './types/category-types'; + +export type { + CartItemsCollection, + CartItemAPIStruct, + NewCartItem, + NewCartItemsCollection, + CartItemDeleteParams, + CartItem, +} from './types/cart-types'; -export type NewDetailedOrder = { - order: NewOrder; - items: NewDetailedOrderItem[]; -}; +export type { + OrderDetailsProduct, + Order, + OrderItem, + DetailedOrder, + OrdersCollection, + DetailedOrderAPIResponse, + DetailedOrdersCollection, + NewOrder, + NewDetailedOrderItem, + NewDetailedOrder, +} from './types/order-types'; diff --git a/app/lib/plamatio-backend/types/cart-types.ts b/app/lib/plamatio-backend/types/cart-types.ts new file mode 100644 index 0000000..06d9421 --- /dev/null +++ b/app/lib/plamatio-backend/types/cart-types.ts @@ -0,0 +1,32 @@ +export type CartItemsCollection = { + data: CartItem[]; +}; + +export type CartItemAPIStruct = { + id: number; + product_id: number; + quantity: number; + user_id: string; +}; + +export type NewCartItem = { + product_id: number; + quantity: number; + user_id: string; +}; + +export type NewCartItemsCollection = { + data: NewCartItem[]; +}; + +export type CartItemDeleteParams = { + cartItemId: number; + userId: string; +}; + +export type CartItem = { + id: number; + product_id: number; + quantity: number; + user_id: string; +}; diff --git a/app/lib/plamatio-backend/types/category-types.ts b/app/lib/plamatio-backend/types/category-types.ts new file mode 100644 index 0000000..ee7408b --- /dev/null +++ b/app/lib/plamatio-backend/types/category-types.ts @@ -0,0 +1,22 @@ +export type Category = { + id: number; + name: string; + description: string; + offered: boolean; +}; + +export type SubCategory = { + id: number; + name: string; + description: string; + category: number; + offered: boolean; +}; + +export type CategoriesCollection = { + data: Category[]; +}; + +export type SubCategoriesCollection = { + data: SubCategory[]; +}; diff --git a/app/lib/plamatio-backend/types/order-types.ts b/app/lib/plamatio-backend/types/order-types.ts new file mode 100644 index 0000000..f2b308e --- /dev/null +++ b/app/lib/plamatio-backend/types/order-types.ts @@ -0,0 +1,71 @@ +export type OrderDetailsProduct = { + id: number; + name: string; + description: string; + price: number; + quantity: number; + imageUrl: string; +}; + +export type Order = { + id: number; + userId: string; + addressId: number; + totalPrice: number; + createdAt: string; + status: string; +}; + +export type OrderItem = { + id: number; + orderId: number; + productId: number; + quantity: number; +}; + +export type DetailedOrder = { + order: Order; + orderItems: OrderItem[]; +}; + +export type OrdersCollection = { + data: Order[]; +}; + +export type DetailedOrderAPIResponse = { + order: { + id: number; + user_id: string; + address_id: number; + total_price: number; + created_at: string; + status: string; + }; + items: { + id: number; + order_id: number; + product_id: number; + quantity: number; + }[]; +}; + +export type DetailedOrdersCollection = { + data: DetailedOrderAPIResponse[]; +}; + +export type NewOrder = { + user_id: string; + address_id: number; + total_price: number; + status: string; +}; + +export type NewDetailedOrderItem = { + product_id: number; + quantity: number; +}; + +export type NewDetailedOrder = { + order: NewOrder; + items: NewDetailedOrderItem[]; +}; diff --git a/app/lib/plamatio-backend/types/product-types.ts b/app/lib/plamatio-backend/types/product-types.ts new file mode 100644 index 0000000..9b3531b --- /dev/null +++ b/app/lib/plamatio-backend/types/product-types.ts @@ -0,0 +1,21 @@ +export type Product = { + id: number; + name: string; + description: string; + category: number; + subCategory: number; + imageUrl: string; + price: number; + previousPrice?: number; + offered: boolean; +}; + +export type CategoryHeroProduct = { + categoryId: number; + subCategoryId: number; + productId: number; +}; + +export type ProductsCollection = { + data: Product[]; +}; diff --git a/app/lib/plamatio-backend/types/user-types.ts b/app/lib/plamatio-backend/types/user-types.ts new file mode 100644 index 0000000..6560fa1 --- /dev/null +++ b/app/lib/plamatio-backend/types/user-types.ts @@ -0,0 +1,41 @@ +export type User = { + id: string; + firstName: string; + lastName: string; + email: string; +}; + +export type Address = { + id: number; + street: string; + city: string; + state: string; + country: string; + zipCode: string; + userId: string; + primary: boolean; +}; + +export type NewUser = { + firstName: string; + lastName: string; + refId: string; +}; + +export type NewAddress = { + street: string; + city: string; + state: string; + country: string; + zipCode: string; + userId: string; +}; + +export type AddressesCollection = { + data: Address[]; +}; + +export type DeleteAddressParams = { + addressId: number; + userId: string; +}; diff --git a/app/lib/plamatio-backend/utils.ts b/app/lib/plamatio-backend/utils.ts new file mode 100644 index 0000000..0a37918 --- /dev/null +++ b/app/lib/plamatio-backend/utils.ts @@ -0,0 +1,21 @@ +/** + * Plamatio Backend API URL + */ +export const getPlamatioBackendAPIURL = () => { + const url = process.env.NEXT_PUBLIC_PLAMATIO_BACKEND_API_URL; + if (!url) { + throw new Error('Plamatio Backend API URL not found'); + } + return url; +}; + +/** + * Plamatio Backend API Key + */ +export const getPlamatioBackendAPIKey = () => { + const apiKey = process.env.NEXT_PUBLIC_PLAMATIO_BACKEND_API_KEY; + if (!apiKey) { + throw new Error('Plamatio Backend API Key not found'); + } + return apiKey; +}; diff --git a/app/lib/store/reducers/cart/cartReducer.ts b/app/lib/store/reducers/cart/cartReducer.ts index b4c0a87..4eb1d9e 100644 --- a/app/lib/store/reducers/cart/cartReducer.ts +++ b/app/lib/store/reducers/cart/cartReducer.ts @@ -1,5 +1,5 @@ import {RootState} from '@/app/lib/store'; -import {CartItem} from '@/app/types/backend-types'; +import {CartItem} from '@/app/lib/plamatio-backend/types'; import {createSlice, PayloadAction} from '@reduxjs/toolkit'; // cart state schema @@ -26,7 +26,7 @@ export const cartSlice = createSlice({ const cartItemToAdd = action.payload; // find if product being added is already in cart const productInCart = cartItems.find( - (cartItem) => cartItem.productId === cartItemToAdd.productId + (cartItem) => cartItem.product_id === cartItemToAdd.product_id ); // if product not in cart, add it to cart if (!productInCart) { @@ -34,7 +34,7 @@ export const cartSlice = createSlice({ } else { // if product already in cart, increment quantity state.cartItems = state.cartItems.map((cartItem) => - cartItem.productId === cartItemToAdd.productId + cartItem.product_id === cartItemToAdd.product_id ? {...cartItem, quantity: cartItem.quantity + 1} : cartItem ); @@ -50,7 +50,7 @@ export const cartSlice = createSlice({ const cartItemToRemove = action.payload; // remove product from cart state.cartItems = cartItems.filter( - (cartItem) => cartItem.productId !== cartItemToRemove.productId + (cartItem) => cartItem.product_id !== cartItemToRemove.product_id ); // update cart items in local storage if (typeof window !== 'undefined') { @@ -63,7 +63,7 @@ export const cartSlice = createSlice({ const cartItemToAdd = action.payload; // find if product being added is already in cart const productInCart = state.cartItems.find( - (cartItem) => cartItem.productId === cartItemToAdd.productId + (cartItem) => cartItem.product_id === cartItemToAdd.product_id ); // if product not in cart, add it to cart if (!productInCart) { @@ -71,7 +71,7 @@ export const cartSlice = createSlice({ } else { // if product already in cart, increment quantity state.cartItems = state.cartItems.map((cartItem) => - cartItem.productId === cartItemToAdd.productId + cartItem.product_id === cartItemToAdd.product_id ? {...cartItem, quantity: cartItem.quantity + 1} : cartItem ); @@ -87,7 +87,7 @@ export const cartSlice = createSlice({ const cartItemToRemove = action.payload; // check if product quantity is 1 const removeProduct = cartItems.find( - (cartItem) => cartItem.productId === cartItemToRemove.productId + (cartItem) => cartItem.product_id === cartItemToRemove.product_id ); console.log( `removing product: ${removeProduct?.id} ${removeProduct?.quantity}` @@ -98,10 +98,11 @@ export const cartSlice = createSlice({ state.cartItems = removeProduct.quantity === 1 ? cartItems.filter( - (cartItem) => cartItem.productId !== cartItemToRemove.productId + (cartItem) => + cartItem.product_id !== cartItemToRemove.product_id ) : cartItems.map((cartItem) => - cartItem.productId === cartItemToRemove.productId + cartItem.product_id === cartItemToRemove.product_id ? {...cartItem, quantity: cartItem.quantity - 1} : cartItem ); @@ -131,6 +132,9 @@ export const cartSlice = createSlice({ localStorage.removeItem('cartItems'); } }, + setNewCartItems: (state, action: PayloadAction) => { + state.cartItems = action.payload; + }, }, }); @@ -147,6 +151,7 @@ export const { decrementQuantity, loadItemsFromLocalStorage, clearCart, + setNewCartItems, enableCartChanges, disableCartChanges, } = cartSlice.actions; diff --git a/app/lib/store/reducers/utils.ts b/app/lib/store/reducers/utils.ts deleted file mode 100644 index 52436c1..0000000 --- a/app/lib/store/reducers/utils.ts +++ /dev/null @@ -1 +0,0 @@ -export const createAction = (type: string, payload = {}) => ({type, payload}); diff --git a/app/lib/stripe/utils.ts b/app/lib/stripe/utils.ts index 8e1f260..25374b6 100644 --- a/app/lib/stripe/utils.ts +++ b/app/lib/stripe/utils.ts @@ -1,12 +1,13 @@ -import {CartItem, Product} from '@/app/types/backend-types'; import Stripe from 'stripe'; import {CURRENCY} from './config'; import { + CartItem, + Product, NewOrder, NewDetailedOrder, NewDetailedOrderItem, + OrderDetailsProduct, } from '@/app/lib/plamatio-backend/types'; -import {OrderDetailsProduct} from '@/app/types/types'; export function formatAmountForDisplay( amount: number, @@ -95,7 +96,7 @@ export function getCartLineItems( products: Product[] ): Stripe.Checkout.SessionCreateParams.LineItem[] { return cartItems.map((cartItem) => { - const product = getProductData(products, cartItem.productId); + const product = getProductData(products, cartItem.product_id); return { price_data: product, quantity: cartItem.quantity, diff --git a/app/page.tsx b/app/page.tsx index 396e921..e1e8f9f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,4 @@ -import LandingPageShowcase from './components/landing/showcase'; +import LandingPageShowcase from './components/landing/Showcase'; export default function Home() { return ( diff --git a/app/types/backend-types.ts b/app/types/backend-types.ts deleted file mode 100644 index 2938f54..0000000 --- a/app/types/backend-types.ts +++ /dev/null @@ -1,99 +0,0 @@ -export type Category = { - id: number; - name: string; - description: string; - offered: boolean; -}; - -export type SubCategory = { - id: number; - name: string; - description: string; - category: number; - offered: boolean; -}; - -export type Product = { - id: number; - name: string; - description: string; - category: number; - subCategory: number; - imageUrl: string; - price: number; - previousPrice?: number; - offered: boolean; -}; - -export type CategoryHeroProduct = { - categoryId: number; - subCategoryId: number; - productId: number; -}; - -export type CartItem = { - id: number; - productId: number; - quantity: number; - userId: string; -}; - -export type NewCartItem = { - productId: number; - quantity: number; - userId: string; -}; - -export type Order = { - id: number; - userId: string; - addressId: number; - totalPrice: number; - createdAt: string; - status: string; -}; - -export type OrderItem = { - id: number; - orderId: number; - productId: number; - quantity: number; -}; - -export type DetailedOrder = { - order: Order; - orderItems: OrderItem[]; -}; - -export type User = { - id: string; - firstName: string; - lastName: string; - email: string; -}; - -export type Address = { - id: number; - street: string; - city: string; - state: string; - country: string; - zipCode: string; - userId: string; - primary: boolean; -}; - -export type NewUser = { - firstName: string; - lastName: string; - refId: string; -}; - -export type NewAddress = { - street: string; - city: string; - state: string; - country: string; - zipCode: string; - userId: string; -}; diff --git a/app/types/types.ts b/app/types/types.ts deleted file mode 100644 index 34c71dc..0000000 --- a/app/types/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type OrderDetailsProduct = { - id: number; - name: string; - description: string; - price: number; - quantity: number; - imageUrl: string; -};