Skip to content

Commit

Permalink
feat: add shopping cart functionality (#51)
Browse files Browse the repository at this point in the history
Co-authored-by: t0fudev <hellenxie09@gmail.com>
  • Loading branch information
evgongora and t0fudev authored Nov 21, 2024
1 parent 327e6be commit e02b53b
Show file tree
Hide file tree
Showing 17 changed files with 358 additions and 226 deletions.
9 changes: 0 additions & 9 deletions apps/web/src/app/_components/features/ProductCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,6 @@ export default function ProductCatalog() {
const [query, setQuery] = useAtom(searchQueryAtom);
const router = useRouter();

const utils = api.useUtils();

const { mutate: addItem } = api.shoppingCart.addItem.useMutation({
onSuccess: async () => {
await utils.shoppingCart.getItems.invalidate();
},
});

// Using an infinite query to fetch products with pagination
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
api.product.getProducts.useInfiniteQuery(
Expand Down Expand Up @@ -99,7 +91,6 @@ export default function ProductCatalog() {
variety={product.name}
price={product.price}
badgeText={product.strength}
isAddingToShoppingCart={false} // Disable shopping cart action for now
onClick={() => accessProductDetails(product.id)} // Trigger add-to-cart action
/>
);
Expand Down
30 changes: 26 additions & 4 deletions apps/web/src/app/_components/features/ProductDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import Button from "@repo/ui/button";
import { ChatWithSeller } from "@repo/ui/chatWithSeller";
import { DataCard } from "@repo/ui/dataCard";
import PageHeader from "@repo/ui/pageHeader";
import { useAtom, useAtomValue } from "jotai";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { addItemAtom, cartItemsAtom } from "~/store/cartAtom";
import { ProducerInfo } from "./ProducerInfo";
import { SelectionTypeCard } from "./SelectionTypeCard";

interface ProductDetailsProps {
product: {
id: number;
image: string;
name: string;
region: string;
Expand All @@ -29,30 +32,48 @@ export default function ProductDetails({ product }: ProductDetailsProps) {
const {
image,
name,
region,
farmName,
roastLevel,
bagsAvailable,
price,
type,
description,
process,
} = product;
const [quantity, setQuantity] = useState(1);
const [isLiked, setIsLiked] = useState(false);
const router = useRouter();
const [isAddingToCart, setIsAddingToCart] = useState(false);
const [, addItem] = useAtom(addItemAtom);
const items = useAtomValue(cartItemsAtom);
const cartItemsCount = items.reduce(
(total, item) => total + item.quantity,
0,
);

const isSoldOut = type === "SoldOut";
const isFarmer = type === "Farmer";

const handleAddToCart = () => {
setIsAddingToCart(true);
addItem({
id: String(product.id),
name: product.name,
quantity: quantity,
price: product.price,
imageUrl: product.image,
});
setIsAddingToCart(false);
};

return (
<div className="flex flex-col items-center mx-auto">
<div className="w-full max-w-[24.375rem]">
<PageHeader
title={<div className="truncate text-xl font-bold">{name}</div>}
showBackButton
onBackClick={() => router.back()}
hideCart={false}
showCart={true}
cartItemsCount={cartItemsCount}
rightActions={
<button
type="button"
Expand Down Expand Up @@ -129,7 +150,8 @@ export default function ProductDetails({ product }: ProductDetailsProps) {
quantity={quantity}
bagsAvailable={bagsAvailable}
onQuantityChange={setQuantity}
onAddToCart={() => void 0}
onAddToCart={handleAddToCart}
isAddingToCart={isAddingToCart}
/>
</div>
)}
Expand Down
23 changes: 12 additions & 11 deletions apps/web/src/app/_components/features/ProductList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { api } from "~/trpc/react";
import { useAtom } from "jotai";
import { addItemAtom } from "~/store/cartAtom";

interface Product {
id: number;
Expand All @@ -14,16 +15,16 @@ interface ProductListProps {
}

export default function ProductList({ products }: ProductListProps) {
const utils = api.useUtils();
const [, addItem] = useAtom(addItemAtom);

const { mutate: addToCart } = api.shoppingCart.addItem.useMutation({
onSuccess: async () => {
await utils.shoppingCart.getItems.invalidate();
},
});

const handleAddToCart = (productId: number) => {
addToCart({ cartId: "1", productId, quantity: 1 });
const handleAddToCart = (product: Product) => {
addItem({
id: String(product.id),
name: product.name,
quantity: 1,
price: product.price,
imageUrl: "/default-image.webp",
});
};

return (
Expand All @@ -38,7 +39,7 @@ export default function ProductList({ products }: ProductListProps) {
${product.price.toFixed(2)}
</p>
<button
onClick={() => handleAddToCart(product.id)}
onClick={() => handleAddToCart(product)}
className="w-full bg-primary text-white py-2 px-4 rounded hover:bg-primary-dark"
type="button"
aria-label={`Add ${product.name} to cart`}
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/app/_components/features/SelectionTypeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface SelectionTypeCardProps {
bagsAvailable: number;
onQuantityChange: (quantity: number) => void;
onAddToCart: () => void;
isAddingToCart?: boolean;
}

export function SelectionTypeCard({
Expand All @@ -17,6 +18,7 @@ export function SelectionTypeCard({
bagsAvailable,
onQuantityChange,
onAddToCart,
isAddingToCart = false,
}: SelectionTypeCardProps) {
const [selectedOption, setSelectedOption] = useState<"bean" | "grounded">(
"bean",
Expand Down Expand Up @@ -75,8 +77,8 @@ export function SelectionTypeCard({
</button>
</div>

<Button variant="primary" onClick={onAddToCart}>
Add to cart
<Button variant="primary" onClick={onAddToCart} disabled={isAddingToCart}>
{isAddingToCart ? "Adding to cart..." : "Add to cart"}
</Button>
</InfoCard>
);
Expand Down
93 changes: 37 additions & 56 deletions apps/web/src/app/_components/features/ShoppingCart.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
"use client";

import { XMarkIcon } from "@heroicons/react/24/solid";
import { api } from "~/trpc/react";
import { useAtom, useAtomValue } from "jotai";
import { useRouter } from "next/navigation";
import { cartItemsAtom, removeItemAtom } from "~/store/cartAtom";

interface ShoppingCartProps {
closeCart: () => void;
}

interface CartItem {
id: string;
product: {
name: string;
price: number;
};
quantity: number;
}

export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
const cartId = "1"; // Assume you have the logic to get the cartId

const utils = api.useUtils();

const { mutate: removeItem } = api.shoppingCart.removeItem.useMutation({
onSuccess: async () => {
await utils.shoppingCart.getItems.invalidate();
},
});
const router = useRouter();
const items = useAtomValue(cartItemsAtom);
const [, removeItem] = useAtom(removeItemAtom);

const handleRemoveItem = (itemId: string) => {
removeItem({ itemId });
removeItem(itemId);
};

const { data: cartItems, isLoading } = api.shoppingCart.getItems.useQuery({
cartId,
});
const handleCheckout = () => {
closeCart();
router.push("/shopping-cart");
};

const totalPrice = items.reduce(
(total, item) => total + item.price * item.quantity,
0,
);

return (
<div className="absolute right-0 top-14 w-96 bg-white p-4 shadow-xl">
Expand All @@ -43,40 +36,28 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
<XMarkIcon className="w-6 text-primary" />
</button>
</div>
{isLoading ? (
<div>Loading...</div>
) : (
<>
<div className="mt-4 flex flex-col gap-4">
{cartItems?.map((item: CartItem) => (
<div key={item.id} className="flex items-center justify-between">
<p>{item.product.name}</p>
<p>${item.product.price}</p>
<button onClick={() => handleRemoveItem(item.id)} type="button">
Remove
</button>
</div>
))}
</div>
<div className="mt-4 flex justify-between">
<p>Total</p>
<p>
$
{cartItems?.reduce(
(total: number, item: CartItem) =>
total + item.product.price * item.quantity,
0,
)}
</p>
<div className="mt-4 flex flex-col gap-4">
{items.map((item) => (
<div key={item.id} className="flex items-center justify-between">
<p>{item.name}</p>
<p>${item.price}</p>
<button onClick={() => handleRemoveItem(item.id)} type="button">
Remove
</button>
</div>
<button
className="mt-4 w-full rounded-xl bg-primary p-4 text-white"
type="button"
>
Checkout
</button>
</>
)}
))}
</div>
<div className="mt-4 flex justify-between">
<p>Total</p>
<p>${totalPrice}</p>
</div>
<button
className="mt-4 w-full rounded-lg bg-primary py-3.5 px-4 text-base font-normal text-white"
type="button"
onClick={handleCheckout}
>
Checkout
</button>
</div>
);
}
11 changes: 10 additions & 1 deletion apps/web/src/app/_components/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
"use client";

import PageHeader from "@repo/ui/pageHeader";
import { useAtomValue } from "jotai";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { cartItemsAtom } from "~/store/cartAtom";

interface HeaderProps {
address: string | undefined;
disconnect: () => void;
showCart?: boolean;
}

function Header({ address, disconnect }: HeaderProps) {
function Header({ address, disconnect, showCart }: HeaderProps) {
const router = useRouter();
const items = useAtomValue(cartItemsAtom);
const cartItemsCount = showCart
? items.reduce((total, item) => total + item.quantity, 0)
: undefined;

const handleLogout = async () => {
await signOut();
Expand All @@ -23,6 +30,8 @@ function Header({ address, disconnect }: HeaderProps) {
title="CofiBlocks"
userAddress={address}
onLogout={handleLogout}
showCart={showCart}
cartItemsCount={cartItemsCount}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function Home() {

return (
<Main>
<Header address={address} disconnect={disconnect} />
<Header address={address} disconnect={disconnect} showCart={true} />
<SearchBar />

{query.length <= 0 && (
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/app/product/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import ProductDetails from "~/app/_components/features/ProductDetails";

interface Product {
id: number;
image: string;
name: string;
region: string;
Expand Down Expand Up @@ -58,6 +59,7 @@ function ProductPage() {
const parsedMetadata = JSON.parse(data.nftMetadata) as NftMetadata;

const product: Product = {
id: Number(id),
image: parsedMetadata.imageUrl,
name: data.name,
region: data.region,
Expand All @@ -66,7 +68,7 @@ function ProductPage() {
bagsAvailable: data.bagsAvailable ?? 10,
price: data.price,
description: parsedMetadata.description,
type: "SoldOut",
type: "Buyer",
process: data.process ?? "Natural",
};

Expand Down
Loading

0 comments on commit e02b53b

Please sign in to comment.