Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add shopping cart functionality #51

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading