From 8edb5e1f5b75b4a437214edef3257442fd99fc75 Mon Sep 17 00:00:00 2001 From: Andymick Date: Thu, 16 Jul 2026 20:10:42 +0100 Subject: [PATCH] Add guest checkout and admin login link - Add guest checkout flow: customers can now complete orders without creating an account - New /checkout page with login or guest options - Guest form collects: full name, email, phone, address - Orders track guest info via guestName and guestPhone fields - Update cart message to reflect guest option - Add "Admin login" link to customer login page for clear navigation - Update checkout API to accept and store guest information Co-Authored-By: Claude Haiku 4.5 --- prisma/schema.prisma | 3 + src/app/account/login/page.tsx | 7 + src/app/api/checkout/route.ts | 11 ++ src/app/cart/page.tsx | 30 +--- src/app/checkout/page.tsx | 249 +++++++++++++++++++++++++++++++++ 5 files changed, 275 insertions(+), 25 deletions(-) create mode 100644 src/app/checkout/page.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 02f27a0..6bee6b7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -187,6 +187,9 @@ model Order { customerId String? // set when the buyer was logged in at checkout — null for guest orders customer Customer? @relation(fields: [customerId], references: [id]) + guestName String? // full name for guest orders + guestPhone String? // contact phone for guest orders + items OrderItem[] } diff --git a/src/app/account/login/page.tsx b/src/app/account/login/page.tsx index c2eda81..e4e2621 100644 --- a/src/app/account/login/page.tsx +++ b/src/app/account/login/page.tsx @@ -56,6 +56,13 @@ export default function LoginPage({ searchParams }: { searchParams: { error?: st Forgot your password?

+ +
+

Shop owner?

+ + Admin login + +
); } diff --git a/src/app/api/checkout/route.ts b/src/app/api/checkout/route.ts index 471fce9..775f94b 100644 --- a/src/app/api/checkout/route.ts +++ b/src/app/api/checkout/route.ts @@ -21,6 +21,13 @@ type CheckoutCartItem = { placementPreviewUrlBack: string | null; }; +type GuestInfo = { + name: string; + email: string; + phone: string; + address: string; +}; + export async function POST(req: Request) { if (!process.env.STRIPE_SECRET_KEY) { return NextResponse.json( @@ -40,6 +47,7 @@ export async function POST(req: Request) { const body = await req.json(); const items: CheckoutCartItem[] = body.items ?? []; + const guest: GuestInfo | undefined = body.guest; if (items.length === 0) { return NextResponse.json({ error: 'Your bag is empty.' }, { status: 400 }); @@ -79,6 +87,9 @@ export async function POST(req: Request) { status: 'PENDING', total, customerId: customer?.id, + email: guest?.email ?? customer?.email, + guestName: guest?.name, + guestPhone: guest?.phone, items: { create: pricedItems.map((i) => ({ productId: i.productId, diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx index 0e9b796..f8d07f9 100644 --- a/src/app/cart/page.tsx +++ b/src/app/cart/page.tsx @@ -7,8 +7,6 @@ import Price from '@/components/Price'; export default function CartPage() { const [mounted, setMounted] = useState(false); - const [checkingOut, setCheckingOut] = useState(false); - const [checkoutError, setCheckoutError] = useState(null); const items = useCart((s) => s.items); const removeItem = useCart((s) => s.removeItem); const setQuantity = useCart((s) => s.setQuantity); @@ -17,24 +15,8 @@ export default function CartPage() { useEffect(() => setMounted(true), []); - const handleCheckout = async () => { - setCheckingOut(true); - setCheckoutError(null); - try { - const res = await fetch('/api/checkout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ items }), - }); - const data = await res.json(); - if (!res.ok || !data.url) { - throw new Error(data.error ?? 'Something went wrong starting checkout.'); - } - window.location.href = data.url; - } catch (err) { - setCheckoutError(err instanceof Error ? err.message : 'Something went wrong.'); - setCheckingOut(false); - } + const handleCheckout = () => { + window.location.href = '/checkout'; }; if (!mounted) return null; // avoid a hydration mismatch on first paint @@ -180,16 +162,14 @@ export default function CartPage() {
- Your cart will be saved. You'll need to log in again to complete your order. + Your cart will be saved. You can log in or continue as a guest to complete your order.
- {checkoutError &&

{checkoutError}

}

You'll enter payment details on the next step.

By placing an order you agree to our{' '} diff --git a/src/app/checkout/page.tsx b/src/app/checkout/page.tsx new file mode 100644 index 0000000..f71e0fc --- /dev/null +++ b/src/app/checkout/page.tsx @@ -0,0 +1,249 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { useCart } from '@/lib/cartStore'; +import { loginCustomer } from '@/app/account/actions'; +import Price from '@/components/Price'; + +export default function CheckoutPage() { + const [mounted, setMounted] = useState(false); + const [mode, setMode] = useState<'login' | 'guest' | null>(null); + const [checkingOut, setCheckingOut] = useState(false); + const [checkoutError, setCheckoutError] = useState(null); + const [guestData, setGuestData] = useState({ + fullName: '', + email: '', + phone: '', + address: '', + }); + + const items = useCart((s) => s.items); + const total = useCart((s) => s.total()); + + useEffect(() => setMounted(true), []); + + if (!mounted) return null; + + if (items.length === 0) { + return ( +

+

Checkout

+

Your bag is empty.

+ + Continue shopping + +
+ ); + } + + const handleGuestCheckout = async (e: React.FormEvent) => { + e.preventDefault(); + if (!guestData.fullName || !guestData.email || !guestData.phone || !guestData.address) { + setCheckoutError('Please fill in all fields.'); + return; + } + + setCheckingOut(true); + setCheckoutError(null); + try { + const res = await fetch('/api/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + items, + guest: { + name: guestData.fullName, + email: guestData.email, + phone: guestData.phone, + address: guestData.address, + }, + }), + }); + const data = await res.json(); + if (!res.ok || !data.url) { + throw new Error(data.error ?? 'Something went wrong starting checkout.'); + } + window.location.href = data.url; + } catch (err) { + setCheckoutError(err instanceof Error ? err.message : 'Something went wrong.'); + setCheckingOut(false); + } + }; + + return ( +
+

Checkout

+ +
+ {/* Order summary */} +
+

Order summary

+
+ {items.map((item) => ( +
+
+

{item.name}

+

Qty: {item.quantity}

+
+ +
+ ))} +
+
+
+ Total + +
+
+
+ + {/* Checkout options */} +
+ {mode === null ? ( +
+

How would you like to checkout?

+ + +

+ Don't have an account?{' '} + + Create one now + +

+
+ ) : mode === 'login' ? ( +
+ +

Log in

+
+ +
+ + +
+
+ + +
+ +
+
+ ) : ( +
+ +

Guest checkout

+ {checkoutError && ( +

+ {checkoutError} +

+ )} +
+
+ + setGuestData({ ...guestData, fullName: e.target.value })} + className="w-full border border-line px-3 py-2 text-sm" + /> +
+
+ + setGuestData({ ...guestData, email: e.target.value })} + className="w-full border border-line px-3 py-2 text-sm" + /> +
+
+ + setGuestData({ ...guestData, phone: e.target.value })} + className="w-full border border-line px-3 py-2 text-sm" + /> +
+
+ +