stripe added, need to test checkout

This commit is contained in:
Jacob Delgado 2024-12-01 02:28:26 -08:00
parent 406890c2aa
commit c449acc56e
7 changed files with 210 additions and 38 deletions

View 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 }
);
}
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CartItem } from '@/lib/store/cart-store'; import type { CartItem } from '@/lib/store/cart-store';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation';
interface CartSidebarProps { interface CartSidebarProps {
items: CartItem[]; items: CartItem[];
@ -18,6 +19,8 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
updateQuantity, updateQuantity,
removeItem, removeItem,
}) => { }) => {
const router = useRouter();
// Count occurrences of each item ID // Count occurrences of each item ID
const itemCounts = items.reduce((acc, item) => { const itemCounts = items.reduce((acc, item) => {
acc[item.id] = (acc[item.id] || 0) + 1; acc[item.id] = (acc[item.id] || 0) + 1;
@ -43,6 +46,32 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
removeItem(itemId); 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 ( return (
<> <>
{/* Overlay */} {/* Overlay */}
@ -140,7 +169,9 @@ const CartSidebar: React.FC<CartSidebarProps> = ({
<span className="font-semibold">${total.toFixed(2)}</span> <span className="font-semibold">${total.toFixed(2)}</span>
</div> </div>
<button <button
onClick={handleCheckout}
className="w-full bg-[#F45B1E] text-white py-2 px-4 rounded-md hover:bg-gray-800" className="w-full bg-[#F45B1E] text-white py-2 px-4 rounded-md hover:bg-gray-800"
disabled={uniqueItems.length === 0}
> >
Checkout Checkout
</button> </button>

View File

@ -4,10 +4,12 @@ import React, { useState, FormEvent } from 'react';
import { import {
PaymentElement, PaymentElement,
useStripe, useStripe,
useElements useElements,
AddressElement
} from '@stripe/react-stripe-js'; } from '@stripe/react-stripe-js';
import { StripePaymentElementOptions } from '@stripe/stripe-js'; import { StripePaymentElementOptions } from '@stripe/stripe-js';
import { useCartStore } from '@/lib/store/cart-store'; import { useCartStore } from '@/lib/store/cart-store';
import Image from 'next/image';
export default function CheckoutForm() { export default function CheckoutForm() {
const stripe = useStripe(); const stripe = useStripe();
@ -15,7 +17,7 @@ export default function CheckoutForm() {
const [isProcessing, setIsProcessing] = useState<boolean>(false); const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const { clearCart } = useCartStore(); const { clearCart, items, getTotalPrice } = useCartStore();
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@ -50,24 +52,139 @@ export default function CheckoutForm() {
}; };
const paymentElementOptions: StripePaymentElementOptions = { 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 ( return (
<form onSubmit={handleSubmit}> <div className="min-h-screen w-full bg-transparent text-gray-900">
<PaymentElement options={paymentElementOptions} /> <form onSubmit={handleSubmit} className="max-w-[2400px] mx-auto">
<button <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 p-8 md:p-12 lg:py-16">
className="w-full py-2 bg-blue-500 text-white rounded" <div className="bg-white rounded-lg border p-8 col-span-2">
disabled={isProcessing || !stripe || !elements} <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
type="submit" {/* Left Column - Payment Method & Order Summary */}
> <div className="space-y-12">
{isProcessing ? 'Processing...' : 'Pay Now'} {/* Payment Method */}
</button> <div>
{message && ( <h2 className="text-xl font-semibold mb-4">Payment Method</h2>
<div className="text-red-500 mt-2"> <PaymentElement
{message} 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> </div>
)} </form>
</form> </div>
); );
} }