Skip to content

Commit

Permalink
#12 [ Frontend ] Cart Products
Browse files Browse the repository at this point in the history
  • Loading branch information
fdhhhdjd committed Oct 22, 2023
1 parent dca93b9 commit 5e45aee
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"prefer-const": "warn",
"max-len": ["error", 200],
"array-bracket-newline": "warn",
"consistent-return": "warn",
"consistent-return": "error",
"eqeqeq": "error",
"no-fallthrough": "off",
"no-unused-expressions": "warn",
Expand Down
16 changes: 16 additions & 0 deletions src/app/carts/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//* IMPORT
import Border from '@/src/components/Border';
import BackButton from '@/src/components/buttons/BackButton';
import CartSection from '@/src/components/sections/CartSection';

const CartsPage = () => {
return (
<>
<BackButton />
<Border />
<CartSection />
</>
);
};

export default CartsPage;
3 changes: 2 additions & 1 deletion src/components/ProductCart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface ProductCartProps {
}

const ProductCart = ({ product }: ProductCartProps) => {
const { id, name, image, sizes, quantity } = product;
const { id, name, image, sizes, quantity, price } = product;

const [size, setSize] = useState<string | undefined>();

Expand All @@ -41,6 +41,7 @@ const ProductCart = ({ product }: ProductCartProps) => {
name,
image,
size,
price,
},
]);
setSize(undefined);
Expand Down
14 changes: 13 additions & 1 deletion src/components/buttons/CheckoutBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { AiOutlineShoppingCart } from 'react-icons/ai';
import { BsFillTrashFill } from 'react-icons/bs';

//* IMPORT
import Button from './Button';
Expand All @@ -22,7 +23,7 @@ const CheckoutBtn = () => {

const [isLoading, setIsLoading] = useState(false);

const { cartItems, setCartItems } = useCartContext();
const { cartItems, setCartItems, clearStorage } = useCartContext();

const toHide = path && (['/carts', '/orders/status'].includes(path) || path.startsWith('/checkout'));

Expand Down Expand Up @@ -59,6 +60,17 @@ const CheckoutBtn = () => {
{cartItems.length ? (
<div className="flex flex-row items-center justify-between gap-1 pb-4 mb-4 border-b border-zinc-300">
<h3 className="text-xl font-black pl-2">Total ({cartItems.length})</h3>
{cartItems.length > 2 && (
<Button
onClick={() => clearStorage()}
isLoading={isLoading}
color="red"
className="py-2 px-6"
>
<BsFillTrashFill />
</Button>
)}

<SeeAllButton route="/carts">Checkout</SeeAllButton>
</div>
) : (
Expand Down
68 changes: 68 additions & 0 deletions src/components/cards/CartCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

//* LIB
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-hot-toast';

//* IMPORT
import cn from '../../helpers/cn';
import { displayNumbers } from '../../helpers/numbers';
import { useCartContext } from '../../providers/CartContextProvider';
import { CartItem } from '../../types/types';
import Button from '../buttons/Button';

export interface CartCardProps {
item: CartItem;
disableAction?: boolean;
}

interface Props extends CartCardProps {
index: number;
}
const CartCard = ({ item, index, disableAction = false }: Props) => {
const { cartItems, setCartItems } = useCartContext();
const router = useRouter();

const [isDeleting, setIsDeleting] = useState(false);

const handleRemove = () => {
setIsDeleting(true);
setCartItems([...cartItems.filter((_, idx) => idx !== index)]);
setTimeout(() => {
setIsDeleting(false);
toast.success('Item successfully removed!');
}, 200);
};

return (
<div className="p-4 border border-zinc-300 bg-gradient-to-tl from-slate-200 grid sm:grid-cols-4 grid-cols-3 mt-2 gap-2">
<div className="col-span-1">
<Image src={item.image} alt={item.id} width={150} height={150} className="w-auto h-auto" />
</div>
<div className="sm:col-span-3 col-span-2">
<p
onClick={() => !disableAction && router.push(`/products/${item.id}`)}
className={cn(
'sm:text-lg text-[16px] font-black ease-in duration-75',
!disableAction && 'hover:underline hover:text-red-800 cursor-pointer'
)}
>
{item.name}
</p>
<p className="text-[14px] font-medium">US M{item.size}</p>
<p className="text-md font-bold text-red-800">${displayNumbers(item.price)}</p>
{!disableAction && (
<div className="flex justify-end">
<Button onClick={() => handleRemove()} isLoading={isDeleting} color="red" className="py-2 px-6">
Remove
</Button>
</div>
)}
</div>
</div>
);
};

export default CartCard;
69 changes: 69 additions & 0 deletions src/components/sections/CartSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client';

//* LIB
import { useRouter } from 'next/navigation';
import { useMemo, useState } from 'react';
import { toast } from 'react-hot-toast';
import { IoArrowForwardOutline } from 'react-icons/io5';

//* IMPORT
import { displayNumbers } from '../../helpers/numbers';
import { useCartContext } from '../../providers/CartContextProvider';
import { useUserContext } from '../../providers/UserProvider';
import Button from '../buttons/Button';
import CartCard from '../cards/CartCard';
import Loader from '../loaders/Loader';
import NotFoundText from '../NotFoundText';

const CartSection = () => {
const { user } = useUserContext();
const { cartItems } = useCartContext();
const [isRedirecting] = useState(false);

const router = useRouter();

const totalPrice = useMemo(() => cartItems.reduce((acc, curr) => acc + (curr?.price ?? 0), 0), [cartItems]);

const handleCheckout = () => {
if (!user || !user.email) toast.error('Please sign-in before checking out');
};

if (isRedirecting) return <Loader />;

return (
<div className="mx-auto max-w-4xl w-full py-4 sm:px-0 px-2">
<h2 className="text-2xl font-black mb-4">Your Cart ({cartItems.length}):</h2>
{cartItems.length > 0 ? (
<>
{cartItems.map((item, idx) => (
<CartCard index={idx} key={item.id + idx} item={item} />
))}

<div className="w-full flex flex-col items-end my-10">
<p className="font-medium mb-2">Your Total</p>
<h2 className="text-4xl font-black mb-4">${displayNumbers(totalPrice)}</h2>
<div className="flex flex-row items-center gap-2">
<Button onClick={() => router.push('/products')} color="secondary" className="px-6 py-3">
Continue Shopping
</Button>

<Button
onClick={() => handleCheckout()}
color="primary"
className="px-6 py-3"
localLoaderOnClick={false}
>
Checkout
<IoArrowForwardOutline className="text-xl ml-2" />
</Button>
</div>
</div>
</>
) : (
<NotFoundText>No Items.</NotFoundText>
)}
</div>
);
};

export default CartSection;
1 change: 1 addition & 0 deletions src/helpers/validations/cartItemSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const cartItemSchema = z.object({
name: z.string(),
image: z.string(),
size: z.string(),
price: z.number(),
});

export const cartsSchema = z.array(cartItemSchema);
16 changes: 14 additions & 2 deletions src/hooks/useLocalStorage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//* LIB
import { useEffect, useState } from 'react';

const useLocalStorage = <T,>(key: string, initialValue: T): [T, (value: T | ((prop: T) => T)) => void] => {
const useLocalStorage = <T,>(key: string, initialValue: T): [T, (value: T | ((prop: T) => T)) => void, () => void] => {
const [storedValue, setStoredValue] = useState<T>(initialValue);

useEffect(() => {
Expand All @@ -25,7 +25,19 @@ const useLocalStorage = <T,>(key: string, initialValue: T): [T, (value: T | ((pr
console.error(error);
}
};
return [storedValue, setValue];

const clearStorage = () => {
try {
setStoredValue(initialValue);
if (typeof window !== 'undefined') {
window.localStorage.removeItem(key);
}
} catch (error) {
console.error(error);
}
};

return [storedValue, setValue, clearStorage];
};

export default useLocalStorage;
5 changes: 4 additions & 1 deletion src/providers/CartContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,18 @@ import { CartItem } from '@/src/types/types';
interface CartContextValues {
cartItems: CartItem[];
setCartItems: (cartItems: CartItem[]) => void;
clearStorage: () => void;
}
export const CartContext = React.createContext<CartContextValues>({
cartItems: [],
setCartItems: () => {},
clearStorage: () => {},
});

export const useCartContext = () => React.useContext(CartContext);

const CartContextProvider = ({ children }: { children: React.ReactNode }) => {
const [cartItems, setCartItems] = useLocalStorage<CartItem[]>('carts', []);
const [cartItems, setCartItems, clearStorage] = useLocalStorage<CartItem[]>('carts', []);

const parsedCartItems = (cartItems: CartItem[]) => {
try {
Expand All @@ -34,6 +36,7 @@ const CartContextProvider = ({ children }: { children: React.ReactNode }) => {
const data = {
cartItems: parsedCartItems(cartItems),
setCartItems,
clearStorage,
};

return <CartContext.Provider value={data}>{children}</CartContext.Provider>;
Expand Down

0 comments on commit 5e45aee

Please sign in to comment.