Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
"use client";
import { useEffect, Suspense, useState } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";
import { createOrder } from "@/actions/checkout";
type OrderItem = {
product_id: string;
product_name: string;
quantity: number;
price: number;
fulfillment: string;
};
type StoredOrder = {
id: string;
customer_name: string;
customer_email: string;
subtotal: number;
status: string;
stop_city: string;
stop_state: string;
stop_date: string;
stop_time: string | null;
stop_location: string;
items: OrderItem[];
};
function shortId(id: string) {
return id.slice(0, 8).toUpperCase();
}
function formatStopDate(dateStr: string): string {
try {
return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
});
} catch {
return dateStr;
}
}
function SuccessContent() {
const searchParams = useSearchParams();
const router = useRouter();
const sessionId = searchParams.get("session_id");
const orderIdParam = searchParams.get("order_id");
const [order, setOrder] = useState<StoredOrder | null>(null);
const [creating, setCreating] = useState(!!sessionId);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (orderIdParam && !sessionId) {
// Direct access with order_id — load from sessionStorage
const stored = sessionStorage.getItem(`order_${orderIdParam}`);
if (stored) {
try {
setOrder(JSON.parse(stored) as StoredOrder);
} catch {
// ignore
}
}
}
}, [orderIdParam, sessionId]);
// Stripe redirected back — create order from pending checkout data
useEffect(() => {
if (!sessionId) return;
const pendingStr = sessionStorage.getItem("pending_checkout");
if (!pendingStr) {
setError("No pending order found. Please start checkout again.");
setCreating(false);
return;
}
let pending: {
customerName: string;
customerEmail: string;
customerPhone: string;
stopId: string | null;
items: Array<{ id: string; name: string; price: number; quantity: number; fulfillment: "pickup" | "ship" }>;
cartBrandId?: string;
shippingAddress?: { state: string; postal_code: string; city?: string };
idempotencyKey: string;
};
try {
pending = JSON.parse(pendingStr);
} catch {
setError("Failed to read checkout data. Please start again.");
setCreating(false);
return;
}
createOrder(
pending.idempotencyKey,
pending.customerName,
pending.customerEmail,
pending.customerPhone,
pending.stopId,
pending.items,
pending.cartBrandId,
pending.shippingAddress
)
.then((result) => {
if (!result.success) {
setError(result.error ?? "Failed to create order");
setCreating(false);
return;
}
sessionStorage.setItem(`order_${result.order.id}`, JSON.stringify(result.order));
sessionStorage.removeItem("pending_checkout");
sessionStorage.removeItem("cart");
setOrder(result.order);
setCreating(false);
})
.catch(() => {
setError("Failed to create order. Please contact support.");
setCreating(false);
});
}, [sessionId]);
if (!order || !orderIdParam) {
return (
<div className="rounded-2xl bg-white p-8 shadow-sm ring-1 ring-slate-200 text-center">
{creating ? (
<>
<div className="h-12 w-12 mx-auto rounded-full border-4 border-slate-200 border-t-slate-900 animate-spin" />
<p className="mt-4 text-slate-600">Confirming your payment and creating order...</p>
</>
) : error ? (
<>
<p className="text-red-600 font-medium">{error}</p>
<Link href="/" className="mt-4 inline-block text-stone-600 hover:text-slate-900">
Back to Storefront
</Link>
</>
) : (
<>
<p className="text-slate-500">Order not found</p>
<Link href="/" className="mt-4 inline-block text-stone-600 hover:text-slate-900">
Back to Storefront
</Link>
</>
)}
</div>
);
}
const pickupItems = order.items.filter((i) => i.fulfillment === "pickup");
const shipItems = order.items.filter((i) => i.fulfillment === "ship");
const hasPickup = pickupItems.length > 0;
const hasShip = shipItems.length > 0;
const isMixed = hasPickup && hasShip;
const stopLabel = [order.stop_city, order.stop_state].filter(Boolean).join(", ");
return (
<div className="mx-auto max-w-2xl">
{/* Header */}
<div className="rounded-2xl bg-white p-8 shadow-sm ring-1 ring-stone-200 text-center">
<svg className="mx-auto h-12 w-12 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 className="mt-6 text-3xl font-bold text-stone-900">
{hasPickup ? "Your pickup order has been placed!" : "Your order has been placed!"}
</h1>
<p className="mt-2 text-base text-stone-500">
Order #{shortId(order.id)} · {order.customer_name}
</p>
</div>
{/* Pickup details */}
{hasPickup && (
<div className="mt-4 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<h2 className="text-lg font-bold text-stone-900 mb-1">
<svg className="inline h-5 w-5 mr-1.5 text-stone-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
Pickup Details
</h2>
{stopLabel && (
<p className="font-semibold text-stone-800">{stopLabel}</p>
)}
{order.stop_date && (
<p className="mt-1 text-stone-600 text-stone-500">
{formatStopDate(order.stop_date)}
{order.stop_time ? ` · ${order.stop_time}` : ""}
</p>
)}
{order.stop_location && (
<p className="mt-1 text-sm text-stone-500">{order.stop_location}</p>
)}
{isMixed && (
<p className="mt-3 text-sm text-stone-500 italic">
See pickup items in the list below.
</p>
)}
{!isMixed && (
<p className="mt-3 text-sm text-stone-500">
Bring your order number or name when you pick up.
</p>
)}
</div>
)}
{/* Shipping details */}
{hasShip && (
<div className="mt-4 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<h2 className="text-lg font-bold text-stone-900 mb-1">
<svg className="inline h-5 w-5 mr-1.5 text-stone-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
Shipping
</h2>
<p className="text-sm text-stone-500">
Tracking information will be sent when your order ships.
</p>
</div>
)}
{/* Order items */}
<div className="mt-4 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<h2 className="text-lg font-bold text-stone-900 mb-4">Order Summary</h2>
{isMixed && hasPickup && (
<div className="mb-4">
<p className="text-sm font-semibold text-stone-500 uppercase tracking-wide mb-2">Pickup Items</p>
<ul className="space-y-2">
{pickupItems.map((item, i) => (
<li key={i} className="flex justify-between text-sm">
<span className="text-stone-700">
{item.quantity > 1 && (
<span className="font-semibold">{item.quantity}× </span>
)}
{item.product_name}
</span>
<span className="text-stone-500">
${(item.price * item.quantity).toFixed(2)}
</span>
</li>
))}
</ul>
</div>
)}
{isMixed && hasShip && (
<div className="mb-4 border-t border-stone-200 pt-4">
<p className="text-sm font-semibold text-stone-500 uppercase tracking-wide mb-2">Shipping Items</p>
<ul className="space-y-2">
{shipItems.map((item, i) => (
<li key={i} className="flex justify-between text-sm">
<span className="text-stone-700">
{item.quantity > 1 && (
<span className="font-semibold">{item.quantity}× </span>
)}
{item.product_name}
</span>
<span className="text-stone-500">
${(item.price * item.quantity).toFixed(2)}
</span>
</li>
))}
</ul>
</div>
)}
{!isMixed && (
<ul className="space-y-2">
{order.items.map((item, i) => (
<li key={i} className="flex justify-between text-sm">
<span className="text-stone-700">
{item.quantity > 1 && (
<span className="font-semibold">{item.quantity}× </span>
)}
{item.product_name}
</span>
<span className="text-stone-500">
${(item.price * item.quantity).toFixed(2)}
</span>
</li>
))}
</ul>
)}
<div className="mt-4 border-t border-slate-200 border-stone-200 pt-4 flex justify-between font-bold text-stone-900">
<span>Total</span>
<span>${order.subtotal.toFixed(2)}</span>
</div>
</div>
{/* Back link */}
<div className="mt-6 text-center">
<Link
href="/"
className="rounded-xl bg-slate-900 bg-stone-800 px-6 py-3 font-medium text-white text-stone-800 hover:bg-slate-800 dark:hover:bg-slate-700"
>
Back to Storefront
</Link>
</div>
</div>
);
}
export default function CheckoutSuccessPage() {
return (
<main className="min-h-screen bg-stone-50 px-6 py-12">
<Suspense fallback={<div className="text-center text-stone-500">Loading...</div>}>
<SuccessContent />
</Suspense>
</main>
);
}