Skip to content

Commit

Permalink
add to cart done
Browse files Browse the repository at this point in the history
  • Loading branch information
fachrihawari committed Sep 22, 2024
1 parent b9d0043 commit 2b27ebb
Show file tree
Hide file tree
Showing 19 changed files with 303 additions and 221 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ MONGO_DATABASE=dbname
TOTAL_PRODUCTS=2000
PORT=3000
NEXT_PUBLIC_URL=http://localhost:3000
JWT_SECRET=secret
35 changes: 35 additions & 0 deletions src/app/(main)/cart/components/CartItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Image from "next/image";
import { formatCurrency } from "@/lib/utils/number";
import { CartItem as CartItemType } from '@/lib/db/cart_collection';
import { CartItemAction } from "./CartItemAction";

type CartItemProps = {
item: CartItemType
}

export function CartItem({ item }: CartItemProps) {
return (
<div className="flex items-center py-6 border-b border-gray-200 last:border-b-0 bg-white rounded-lg mb-4 overflow-hidden transition-all duration-300">
<div className="flex-shrink-0 w-24 h-24 bg-gray-100 rounded-md overflow-hidden relative ml-4">
<Image
src={item.thumbnail}
alt={item.name}
layout="fill"
objectFit="cover"
className="transition-transform duration-300 hover:scale-105"
/>
</div>
<div className="ml-6 flex-1">
<h3 className="text-lg font-semibold text-gray-800">{item.name}</h3>
<div className="mt-2 flex items-center space-x-4">
<p className="text-sm text-gray-600">Unit Price: <span className="text-gray-800 font-medium">{formatCurrency(item.price)}</span></p>
<p className="text-sm text-gray-600">Quantity: <span className="text-gray-800 font-medium">{item.quantity}</span></p>
</div>
<p className="mt-2 text-md text-gray-700">Total: <span className="text-gray-900 font-bold">{formatCurrency(item.price * item.quantity)}</span></p>
</div>
<div className="mr-4">
<CartItemAction item={item} />
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions src/app/(main)/cart/components/CartItemAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'

import { FiMinus, FiPlus, FiTrash2 } from "react-icons/fi";
import type { CartItem } from "@/lib/db/cart_collection";
import { removeFromCart, updateCartItemQuantity } from "@/lib/actions/cart";

type CartItemActionProps = {
item: CartItem
}

export function CartItemAction({ item }: CartItemActionProps) {
return (
<div className="flex items-center">
<button
type="submit"
className="text-gray-500 hover:text-gray-600"
onClick={() => {
if (item.quantity <= 1) {
removeFromCart(item.productId.toString())
} else {
updateCartItemQuantity(item.productId.toString(), item.quantity - 1)
}
}}
>
<FiMinus />
</button>
<span className="mx-2 text-gray-700">{item.quantity}</span>
<button
type="submit"
className="text-gray-500 hover:text-gray-600"
onClick={() => updateCartItemQuantity(item.productId.toString(), item.quantity + 1)}
>
<FiPlus />
</button>
<button
type="submit"
className="ml-4 text-red-500 hover:text-red-600"
onClick={() => removeFromCart(item.productId.toString())}
>
<FiTrash2 />
</button>
</div>
)
}
72 changes: 11 additions & 61 deletions src/app/(main)/cart/components/CartItems.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,22 @@
'use client';

import { useState } from 'react';
import { formatCurrency } from "@/lib/utils/number";
import Image from "next/image";
import { FiMinus, FiPlus, FiTrash2 } from "react-icons/fi";
import { updateCartItemQuantity, removeFromCart } from '@/lib/actions/cart';
import { Product } from '@/lib/db/product_collection';
import type { CartItem as CartItemType } from "@/lib/db/cart_collection";
import { CartItem } from "./CartItem";

type CartItem = {
product: Product;
quantity: number;
type CartItemsProps = {
cartItems: CartItemType[]
}

export default function CartItems({ initialCart }: { initialCart: CartItem[] }) {
const [cart, setCart] = useState(initialCart);

const handleUpdateQuantity = async (productId: string, newQuantity: number) => {
if (newQuantity === 0) {
await removeFromCart(productId);
setCart(cart.filter(item => item.product._id?.toString() !== productId));
} else {
await updateCartItemQuantity(productId, newQuantity);
setCart(cart.map(item =>
item.product._id?.toString() === productId ? { ...item, quantity: newQuantity } : item
));
}
};

const handleRemoveItem = async (productId: string) => {
await removeFromCart(productId);
setCart(cart.filter(item => item.product._id?.toString() !== productId));
};

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

export default function CartItems({ cartItems }: CartItemsProps) {
const totalPrice = cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-2xl font-bold mb-4">Products</h2>
{cart.map((item) => (
<div key={item.product._id?.toString()} className="flex items-center py-4 border-b border-gray-200 last:border-b-0">
<Image src={item.product.thumbnail} alt={item.product.name} width={80} height={80} className="rounded-md" />
<div className="ml-4 flex-grow">
<h3 className="font-semibold">{item.product.name}</h3>
<p className="text-gray-600">{formatCurrency(item.product.price)}</p>
</div>
<div className="flex items-center">
<button
onClick={() => handleUpdateQuantity(item.product._id?.toString()!, item.quantity - 1)}
className="text-gray-500 hover:text-gray-700 p-1"
>
<FiMinus />
</button>
<span className="mx-2 w-8 text-center">{item.quantity}</span>
<button
onClick={() => handleUpdateQuantity(item.product._id?.toString()!, item.quantity + 1)}
className="text-gray-500 hover:text-gray-700 p-1"
>
<FiPlus />
</button>
</div>
<button
onClick={() => handleRemoveItem(item.product._id?.toString()!)}
className="ml-4 text-red-500 hover:text-red-700 p-1"
>
<FiTrash2 />
</button>
</div>
{cartItems.map((item) => (
<CartItem
key={item.productId.toString()}
item={item}
/>
))}
</div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
Expand Down
13 changes: 6 additions & 7 deletions src/app/(main)/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import { cookies } from 'next/headers';
import CartNotLoggedIn from './components/CartNotLoggedIn';
import CartEmpty from './components/CartEmpty';
import CartItems from './components/CartItems';
import { getCart } from '@/lib/actions/cart';
import { isLoggedIn } from '@/lib/utils/auth';

export default async function CartPage() {
const isLoggedIn = cookies().get('token'); // Replace this with actual authentication check
const cart = await getCart();

let content;

if (!isLoggedIn) {
if (!isLoggedIn()) {
content = <CartNotLoggedIn />
} else if (cart.length === 0) {
} else if (cart.items.length === 0) {
content = <CartEmpty />
} else {
content = <CartItems initialCart={cart} />
content = <CartItems cartItems={cart.items} />
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/app/(main)/products/components/ProductsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function ProductsList({ searchParams }: ProductsListProps) {
{products.length > 0 ?
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">
{products.map((product) => (
<ProductCard key={product._id?.toString()} product={product} />
<ProductCard key={product._id.toString()} product={product} />
))}
</div>
:
Expand Down
92 changes: 92 additions & 0 deletions src/app/api/cart/[productId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { CartItemSchema, cartsCollection } from "@/lib/db/cart_collection";
import { productsCollection } from "@/lib/db/product_collection";
import { NextRequest, NextResponse } from "next/server";
import { ObjectId } from "mongodb";
import { z } from "zod";

const CartItemDto = z.object({
quantity: z.number()
})
const CartParamsDto = z.object({
productId: z.string()
})

type NextRouteParams = {
params: {
productId: string
}
}

export async function POST(req: NextRequest, { params }: NextRouteParams) {
try {
const { productId } = CartParamsDto.parse(params)
const userId = String(req.headers.get("x-user-id"))
const rawBody = await req.json();
const { quantity } = CartItemDto.parse(rawBody)

const product = await productsCollection.findOne({ _id: new ObjectId(productId) });
if (!product) {
return NextResponse.json({ errors: ["Product not found"] }, { status: 404 });
}

const cartItem = CartItemSchema.parse({
productId: new ObjectId(product._id),
name: product.name,
price: product.price,
thumbnail: product.thumbnail,
quantity: quantity,
});

await cartsCollection.updateOne(
{ userId: new ObjectId(userId) },
{ $push: { items: cartItem } },
{ upsert: true }
);

return NextResponse.json({ message: "Product added to cart successfully" });
} catch (error) {
return NextResponse.json({ errors: ["An unexpected error occurred"] }, { status: 500 });
}
}

export async function PUT(req: NextRequest, { params }: NextRouteParams) {
try {
const { productId } = CartParamsDto.parse(params)
const userId = String(req.headers.get("x-user-id"));
const rawBody = await req.json();
const { quantity } = CartItemDto.parse(rawBody)

await cartsCollection.updateOne(
{
userId: new ObjectId(userId),
"items.productId": new ObjectId(productId)
},
{
$set: {
"items.$.quantity": quantity
}
}
);

return NextResponse.json({ message: "Product qty updated in cart successfully" });
} catch (error) {
return NextResponse.json({ errors: ["An unexpected error occurred"] }, { status: 500 });
}
}

export async function DELETE(req: NextRequest, { params }: NextRouteParams) {
try {
const userId = String(req.headers.get("x-user-id"));

const { productId } = CartParamsDto.parse(params)

await cartsCollection.updateOne(
{ userId: new ObjectId(userId) },
{ $pull: { items: { productId: new ObjectId(productId) } } }
);

return NextResponse.json({ message: "Product removed from cart successfully" });
} catch (error) {
return NextResponse.json({ errors: ["An unexpected error occurred"] }, { status: 500 });
}
}
16 changes: 16 additions & 0 deletions src/app/api/cart/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cartsCollection } from "@/lib/db/cart_collection";
import { NextRequest, NextResponse } from "next/server";
import { ObjectId } from "mongodb";

export async function GET(req: NextRequest) {
try {
const userId = String(req.headers.get("x-user-id"))
const cart = await cartsCollection.findOne({ userId: new ObjectId(userId) });
if (!cart) {
return NextResponse.json({ errors: ["Cart not found"] }, { status: 404 });
}
return NextResponse.json(cart);
} catch (error) {
return NextResponse.json({ errors: ["An unexpected error occurred"] }, { status: 500 });
}
}
Loading

0 comments on commit 2b27ebb

Please sign in to comment.