Skip to content

Commit

Permalink
Code to handle synchronization between local and DB user cart data (#8)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pranav-kural authored Sep 12, 2024
1 parent 4dc1a3a commit 7604410
Show file tree
Hide file tree
Showing 81 changed files with 2,063 additions and 398 deletions.
2 changes: 1 addition & 1 deletion app/(data)/sample-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
SubCategory,
Product,
CategoryHeroProduct,
} from '../types/backend-types';
} from '@/app/lib/plamatio-backend/types';

const categories: Category[] = [
{
Expand Down
4 changes: 2 additions & 2 deletions app/(routes)/category/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
8 changes: 4 additions & 4 deletions app/(routes)/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion app/components/addresses/SelectAddressModal.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
32 changes: 32 additions & 0 deletions app/components/auth/SigInSignUpButtons.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-row items-center justify-center rounded-lg">
<SignedOut>
<Link
href="/auth/sign-in"
className="text-center text-violet-900 hover:bg-violet-50 rounded-lg px-2 py-1 md:px-3 md:py-2">
<span className={`font-[500]`}>Sign in</span>
</Link>
<Link
href="/auth/sign-up"
className="bg-violet-900 hover:bg-violet-800 text-white rounded-lg text-center px-2 py-1 md:px-3 md:py-2">
Sign up
</Link>
</SignedOut>
<SignedIn>
<SignOutButton>
<div className="w-full flex flex-row gap-2 text-violet-900 text-md items-center cursor-pointer hover:bg-violet-50 px-2 py-1 md:px-3 md:py-2">
<span>Sign Out</span>
<User2Icon size={20} />
</div>
</SignOutButton>
</SignedIn>
</div>
);
};

export default SignInSignUpButtons;
103 changes: 103 additions & 0 deletions app/components/cart/AddToCartButton.tsx
Original file line number Diff line number Diff line change
@@ -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<AddToCartButtonProps> = (
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 (
<>
<Toast
title="Added to Cart"
visible={toastVisible}
setVisible={setToastVisible}
description={`${props.product.name} has been added to your cart.`}
/>
<button
className={classNames(
'flex flex-row align-middle justify-center p-2 rounded-md bg-violet-100 cursor-pointer hover:text-violet-100 hover:bg-violet-800',
props.className
)}
onClick={handleAddToCart}>
{props.showLabel && (
<span
className={classNames('text-lg ml-3 mr-3', props.labelClassName)}>
Add to Cart
</span>
)}
<PlusIcon />
<ShoppingCartIcon />
</button>
</>
);
};

export default AddToCartButton;
42 changes: 42 additions & 0 deletions app/components/cart/CartButton.tsx
Original file line number Diff line number Diff line change
@@ -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<CartButtonProps> = (props) => {
// get user id
const {isLoaded, user} = useUser();
// if user is not available
return (
<>
{!isLoaded ? (
<LoadingSpinner />
) : (
<>
<StatefulCartButton
userId={user?.id}
product={props.product}
showLabel={props.showLabel}
className={props.className}
labelClassName={props.labelClassName}
/>
{user && <MergeCartItems userId={user.id} />}
</>
)}

{/* If user signed in, merge cart items if any difference between local cart items & database */}
</>
);
};

export default CartButton;
44 changes: 44 additions & 0 deletions app/components/cart/CartIcon.tsx
Original file line number Diff line number Diff line change
@@ -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<CartIconProps> = ({
className,
iconSize,
strokeWidth,
iconClassName,
}) => {
const [displayCartWindow, setDisplayCartWindow] = useState(false);

return (
<>
<div className={classNames('', className)}>
<button
onClick={() => setDisplayCartWindow(!displayCartWindow)}
className="text-violet-900">
<ShoppingBagIcon
size={iconSize || 35}
strokeWidth={strokeWidth || 1}
className={classNames('', iconClassName)}
/>
</button>
</div>
{displayCartWindow && (
<div className="z-10 absolute top-[50px] right-[10px] md:right-[0px] w-auto">
<CartWindow />
</div>
)}
</>
);
};

export default CartIcon;
Loading

0 comments on commit 7604410

Please sign in to comment.