stripe added, need to test checkout
This commit is contained in:
parent
406890c2aa
commit
c449acc56e
33
frontend/src/app/api/create-payment-intent/route.ts
Normal file
33
frontend/src/app/api/create-payment-intent/route.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: '2024-11-20.acacia'
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { items } = body;
|
||||
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: Math.round(items.reduce((sum: number, item: any) =>
|
||||
sum + (item.price * item.quantity), 0) * 100),
|
||||
currency: 'usd',
|
||||
payment_method_types: ['card', 'paypal', 'apple_pay', 'google_pay'],
|
||||
automatic_payment_methods: {
|
||||
enabled: false
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
clientSecret: paymentIntent.client_secret
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Stripe error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Error creating payment intent' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
12
frontend/src/app/checkout/page.tsx
Normal file
12
frontend/src/app/checkout/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import StripeCheckout from '@/lib/stripe/checkout';
|
||||
|
||||
export default function CheckoutPage() {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">Checkout</h1>
|
||||
<StripeCheckout />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
// src/app/(routes)/cart/page.tsx
|
||||
'use client';
|
||||
|
||||
import { useCartStore } from '@/lib/store/cart-store';
|
||||
import StripeCheckout from '@/lib/stripe/checkout';
|
||||
|
||||
export default function CartPage() {
|
||||
const { items, getTotalPrice } = useCartStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Your Cart</h1>
|
||||
{items.map(item => (
|
||||
<div key={item.id}>
|
||||
{item.title} - ${item.price} x {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
<StripeCheckout />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { CartItem } from '@/lib/store/cart-store';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface CartSidebarProps {
|
||||
items: CartItem[];
|
||||
@ -18,6 +19,8 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
|
||||
updateQuantity,
|
||||
removeItem,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
// Count occurrences of each item ID
|
||||
const itemCounts = items.reduce((acc, item) => {
|
||||
acc[item.id] = (acc[item.id] || 0) + 1;
|
||||
@ -43,6 +46,32 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
|
||||
removeItem(itemId);
|
||||
}
|
||||
|
||||
const handleCheckout = async () => {
|
||||
try {
|
||||
// Create payment intent
|
||||
const response = await fetch('/api/create-payment-intent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ items: uniqueItems }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
// Close the cart sidebar
|
||||
onClose();
|
||||
|
||||
// Redirect to checkout page
|
||||
router.push('/checkout');
|
||||
} catch (error) {
|
||||
console.error('Error initiating checkout:', error);
|
||||
// You might want to show an error message to the user here
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Overlay */}
|
||||
@ -140,7 +169,9 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
|
||||
<span className="font-semibold">${total.toFixed(2)}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
className="w-full bg-[#F45B1E] text-white py-2 px-4 rounded-md hover:bg-gray-800"
|
||||
disabled={uniqueItems.length === 0}
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
|
||||
@ -4,10 +4,12 @@ import React, { useState, FormEvent } from 'react';
|
||||
import {
|
||||
PaymentElement,
|
||||
useStripe,
|
||||
useElements
|
||||
useElements,
|
||||
AddressElement
|
||||
} from '@stripe/react-stripe-js';
|
||||
import { StripePaymentElementOptions } from '@stripe/stripe-js';
|
||||
import { useCartStore } from '@/lib/store/cart-store';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function CheckoutForm() {
|
||||
const stripe = useStripe();
|
||||
@ -15,7 +17,7 @@ export default function CheckoutForm() {
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const { clearCart } = useCartStore();
|
||||
const { clearCart, items, getTotalPrice } = useCartStore();
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@ -50,24 +52,139 @@ export default function CheckoutForm() {
|
||||
};
|
||||
|
||||
const paymentElementOptions: StripePaymentElementOptions = {
|
||||
layout: 'tabs',
|
||||
layout: {
|
||||
type: 'tabs',
|
||||
defaultCollapsed: false,
|
||||
radios: false,
|
||||
spacedAccordionItems: false
|
||||
},
|
||||
paymentMethodOrder: [ 'card', 'apple_pay', 'google_pay', 'paypal', 'amazon_pay']
|
||||
};
|
||||
|
||||
const total = getTotalPrice({ items });
|
||||
const shipping = 5.99;
|
||||
const taxRate = 0.0825;
|
||||
const subtotal = total;
|
||||
const tax = subtotal * taxRate;
|
||||
const finalTotal = subtotal + shipping + tax;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<PaymentElement options={paymentElementOptions} />
|
||||
<button
|
||||
className="w-full py-2 bg-blue-500 text-white rounded"
|
||||
disabled={isProcessing || !stripe || !elements}
|
||||
type="submit"
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Pay Now'}
|
||||
</button>
|
||||
{message && (
|
||||
<div className="text-red-500 mt-2">
|
||||
{message}
|
||||
<div className="min-h-screen w-full bg-transparent text-gray-900">
|
||||
<form onSubmit={handleSubmit} className="max-w-[2400px] mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 p-8 md:p-12 lg:py-16">
|
||||
<div className="bg-white rounded-lg border p-8 col-span-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Left Column - Payment Method & Order Summary */}
|
||||
<div className="space-y-12">
|
||||
{/* Payment Method */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Payment Method</h2>
|
||||
<PaymentElement
|
||||
options={paymentElementOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Order Summary</h2>
|
||||
<div className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex items-center space-x-4 py-4 border-b last:border-b-0">
|
||||
<div className="w-20 h-20 flex-shrink-0 relative bg-white rounded-md overflow-hidden">
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_UPLOAD_URL + item.image}`}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="80px"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<h3 className="font-medium">{item.title}</h3>
|
||||
<p className="text-sm text-gray-600">Quantity: {item.quantity}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">${(item.price * item.quantity).toFixed(2)}</p>
|
||||
<p className="text-sm text-gray-600">${item.price.toFixed(2)} each</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t pt-4 ">
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span>${total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{message && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 p-4 rounded-lg">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column - Shipping Address & Total Breakdown */}
|
||||
<div className="space-y-12 lg:sticky lg:top-8">
|
||||
{/* Shipping Address */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Shipping Address</h2>
|
||||
<AddressElement
|
||||
options={{
|
||||
mode: 'shipping',
|
||||
allowedCountries: ['US'],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Order Total Breakdown */}
|
||||
<div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Subtotal</span>
|
||||
<span>${subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Shipping</span>
|
||||
<span>${shipping.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Tax</span>
|
||||
<span>${tax.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-semibold pt-3 border-t">
|
||||
<span>Total</span>
|
||||
<span>${finalTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
className="w-full py-3 bg-blue-600 text-white rounded-lg font-semibold
|
||||
hover:bg-blue-700 transition-colors disabled:bg-gray-400
|
||||
disabled:cursor-not-allowed"
|
||||
disabled={isProcessing || !stripe || !elements}
|
||||
type="submit"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<svg className="animate-spin h-5 w-5 mr-3" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Processing...
|
||||
</div>
|
||||
) : (
|
||||
`Pay $${finalTotal.toFixed(2)}`
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user