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 6 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
17 changes: 12 additions & 5 deletions apps/web/src/app/_components/features/ShoppingCart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { XMarkIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { api } from "~/trpc/react";

interface ShoppingCartProps {
Expand All @@ -17,8 +18,8 @@ interface CartItem {
}

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

const router = useRouter();
const cartId = "1";
rvalenciano marked this conversation as resolved.
Show resolved Hide resolved
const utils = api.useUtils();

const { mutate: removeItem } = api.shoppingCart.removeItem.useMutation({
Expand All @@ -31,6 +32,11 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
removeItem({ itemId });
};

const handleCheckout = () => {
closeCart();
router.push("/shopping-cart");
};

const { data: cartItems, isLoading } = api.shoppingCart.getItems.useQuery({
cartId,
});
Expand All @@ -48,7 +54,7 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
) : (
<>
<div className="mt-4 flex flex-col gap-4">
{cartItems?.map((item: CartItem) => (
{cartItems?.items.map((item: CartItem) => (
<div key={item.id} className="flex items-center justify-between">
<p>{item.product.name}</p>
<p>${item.product.price}</p>
Expand All @@ -62,16 +68,17 @@ export default function ShoppingCart({ closeCart }: ShoppingCartProps) {
<p>Total</p>
<p>
$
{cartItems?.reduce(
{cartItems?.items.reduce(
(total: number, item: CartItem) =>
total + item.product.price * item.quantity,
0,
)}
</p>
</div>
<button
className="mt-4 w-full rounded-xl bg-primary p-4 text-white"
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>
Expand Down
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
10 changes: 8 additions & 2 deletions apps/web/src/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@

import Carousel from "@repo/ui/carousel";
import { useAccount, useDisconnect } from "@starknet-react/core";
import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import ProductCatalog from "~/app/_components/features/ProductCatalog";
import Header from "~/app/_components/layout/Header";
import Main from "~/app/_components/layout/Main";
import { searchQueryAtom } from "~/atoms/productAtom";
import { cartItemsAtom } from "~/store/cartAtom";
import SearchBar from "../_components/features/SearchBar";

export default function Home() {
const { t } = useTranslation();
const { address } = useAccount();
const { disconnect } = useDisconnect();
const [query] = useAtom(searchQueryAtom);
const items = useAtomValue(cartItemsAtom);
const cartItemsCount = items.reduce(
(total, item) => total + item.quantity,
0,
);

const carouselData = [
{
Expand All @@ -39,7 +45,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