-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
adea126
commit 6be5714
Showing
21 changed files
with
392 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
export default function CartItemsPlaceholder() { | ||
return ( | ||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | ||
<div className="md:col-span-2 bg-white rounded-lg flex flex-col gap-4"> | ||
{[...new Array(3)].map((_, index) => ( | ||
<div key={index} className="flex items-center p-4 border border-gray-200 rounded-lg mb-4 last:mb-0"> | ||
<div className="w-20 h-20 bg-gray-200 rounded-md mr-4"></div> | ||
<div className="flex-grow"> | ||
<div className="h-5 bg-gray-200 rounded w-3/4 mb-2"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-2"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
</div> | ||
<div className="flex flex-col items-end"> | ||
<div className="h-5 bg-gray-200 rounded w-20 mb-2"></div> | ||
<div className="h-8 bg-gray-200 rounded w-24"></div> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-24 self-start"> | ||
<div className="h-6 bg-gray-200 rounded w-3/4 mb-4"></div> | ||
<div className="space-y-4"> | ||
<div className="flex justify-between items-center"> | ||
<div className="h-4 bg-gray-200 rounded w-1/4"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/6"></div> | ||
</div> | ||
<div className="flex justify-between items-center"> | ||
<div className="h-4 bg-gray-200 rounded w-1/5"></div> | ||
<div className="h-4 bg-gray-200 rounded w-1/8"></div> | ||
</div> | ||
<div className="pt-4 border-t border-gray-200"> | ||
<div className="flex justify-between items-center"> | ||
<div className="h-5 bg-gray-200 rounded w-1/4"></div> | ||
<div className="h-5 bg-gray-200 rounded w-1/5"></div> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="h-10 bg-gray-200 rounded w-full mt-6"></div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
'use client' | ||
|
||
import { formatCurrency } from "@/lib/utils/number"; | ||
import type { CartItem as CartItemType } from "@/lib/db/cart_collection"; | ||
import { checkout } from "@/lib/actions/orders"; | ||
import { useEffect } from "react"; | ||
import { useFormState, useFormStatus } from "react-dom"; | ||
import { useMidtrans } from "@/lib/hooks/useMidtrans"; | ||
|
||
type CartSummaryProps = { | ||
cartItems: CartItemType[] | ||
} | ||
|
||
function CheckoutButton() { | ||
const { pending } = useFormStatus() | ||
const className = pending ? "bg-gray-500" : "bg-green-500 hover:bg-green-600 " | ||
return ( | ||
<button disabled={pending} className={`mt-6 w-full text-white py-3 rounded-lg transition-colors duration-300 font-semibold ${className}`}> | ||
{pending ? 'Loading...' : 'Proceed to Checkout'} | ||
</button> | ||
) | ||
} | ||
|
||
type CheckoutState = { | ||
paymentToken: string | ||
} | ||
|
||
export default function CartSummary({ cartItems }: CartSummaryProps) { | ||
const { isReady, pay } = useMidtrans({ | ||
onClose: () => { | ||
console.log("close") | ||
} | ||
}) | ||
const [checkoutState, handleCheckout] = useFormState<CheckoutState>(checkout, { | ||
paymentToken: "" | ||
}) | ||
const totalPrice = cartItems.reduce((total, item) => total + item.price * item.quantity, 0); | ||
|
||
useEffect(() => { | ||
if (checkoutState.paymentToken && isReady) { | ||
pay(checkoutState.paymentToken) | ||
} | ||
}, [checkoutState.paymentToken, isReady, pay]) | ||
|
||
return ( | ||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 sticky top-24 self-start"> | ||
<h2 className="text-2xl font-bold mb-4">Order Summary</h2> | ||
<div className="space-y-4"> | ||
<div className="flex justify-between items-center"> | ||
<span className="text-gray-600">Subtotal</span> | ||
<span className="font-semibold">{formatCurrency(totalPrice)}</span> | ||
</div> | ||
<div className="flex justify-between items-center"> | ||
<span className="text-gray-600">Shipping</span> | ||
<span className="font-semibold">Free</span> | ||
</div> | ||
<div className="border-t border-gray-200 pt-4"> | ||
<div className="flex justify-between items-center"> | ||
<span className="text-lg font-semibold">Total</span> | ||
<span className="text-2xl font-bold text-green-600">{formatCurrency(totalPrice)}</span> | ||
</div> | ||
</div> | ||
</div> | ||
<form action={handleCheckout}> | ||
<CheckoutButton /> | ||
</form> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import CartPlaceholder from "./components/CartPlaceholder"; | ||
|
||
export default function CartLoading() { | ||
return ( | ||
<div className="container mx-auto px-4 py-8"> | ||
<h1 className="text-2xl font-bold mb-6">Shopping Cart</h1> | ||
<CartPlaceholder /> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { ObjectId } from "mongodb"; | ||
import { OrderForm, OrderSchema, ordersCollection } from "@/lib/db/order_collection"; | ||
import { NextRequest, NextResponse } from "next/server"; | ||
import { cartsCollection } from "@/lib/db/cart_collection"; | ||
import midtransClient from "midtrans-client"; | ||
import { usersCollection } from "@/lib/db/user_collection"; | ||
|
||
export async function GET(req: NextRequest) { | ||
const userId = String(req.headers.get("x-user-id")) | ||
const orders = await ordersCollection.find({ userId: new ObjectId(userId) }).toArray() | ||
return NextResponse.json(orders) | ||
} | ||
|
||
export async function POST(req: NextRequest) { | ||
const userId = String(req.headers.get("x-user-id")) | ||
|
||
try { | ||
const user = await usersCollection.findOne({ _id: new ObjectId(userId) }) | ||
if (!user) { | ||
return NextResponse.json({ error: "User not found" }, { status: 404 }) | ||
} | ||
|
||
const cart = await cartsCollection.findOne({ userId: new ObjectId(userId) }) | ||
if (!cart) { | ||
return NextResponse.json({ error: "Cart not found" }, { status: 404 }) | ||
} | ||
|
||
// Create a new order | ||
const newOrder: OrderForm = { | ||
userId: new ObjectId(userId), | ||
products: cart.items, | ||
total: cart.items.reduce((acc, item) => acc + item.price * item.quantity, 0), | ||
status: "pending", | ||
createdAt: new Date(), | ||
updatedAt: new Date() | ||
}; | ||
|
||
OrderSchema.parse(newOrder) | ||
|
||
// Insert the order into the database | ||
const result = await ordersCollection.insertOne(newOrder); | ||
|
||
// Prepare Midtrans payment request | ||
const snap = new midtransClient.Snap({ | ||
isProduction: process.env.NODE_ENV === "production", | ||
serverKey: process.env.MIDTRANS_SERVER_KEY, | ||
clientKey: process.env.MIDTRANS_CLIENT_KEY | ||
}); | ||
|
||
const transactionDetails = { | ||
order_id: result.insertedId.toString(), | ||
gross_amount: newOrder.total | ||
}; | ||
|
||
const itemDetails = newOrder.products.map((product: any) => ({ | ||
id: product.productId, | ||
price: product.price, | ||
quantity: product.quantity, | ||
name: product.name | ||
})); | ||
|
||
const customerDetails = { | ||
first_name: user.name, | ||
email: user.email | ||
}; | ||
|
||
const midtransParameter = { | ||
transaction_details: transactionDetails, | ||
item_details: itemDetails, | ||
customer_details: customerDetails | ||
}; | ||
|
||
// Create Midtrans transaction | ||
const transaction = await snap.createTransaction(midtransParameter); | ||
|
||
// Return the Midtrans token | ||
return NextResponse.json({ | ||
orderId: result.insertedId, | ||
paymentToken: transaction.token | ||
}, { status: 201 }); | ||
|
||
} catch (error) { | ||
return NextResponse.json({ error: "Failed to create order" }, { status: 500 }); | ||
} | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
'use server' | ||
|
||
import { cookies } from "next/headers"; | ||
import { setQueryParams } from "../utils/url"; | ||
import { redirect } from "next/navigation"; | ||
|
||
const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_URL | ||
|
||
export const checkout = async () => { | ||
await new Promise(resolve => setTimeout(resolve, 3000)) | ||
return { | ||
paymentToken: "f39dbe10-2dc5-4b41-8359-a17d6dc2e5eb" | ||
} | ||
|
||
const res = await fetch(`${NEXT_PUBLIC_URL}/api/orders`, { | ||
method: 'POST', | ||
headers: { | ||
Cookie: cookies().toString() | ||
} | ||
}); | ||
|
||
if (!res.ok) { | ||
const errorQuery = setQueryParams({ type: 'error', title: "Error", message: "Failed to checkout" }); | ||
return redirect('/cart?' + errorQuery); | ||
} | ||
|
||
const data = await res.json() | ||
return data | ||
} |
Oops, something went wrong.