Refactor: move public storefront stop data to server-side + parallel agent work
Server-side / caching refactor (Grok): - New RPC get_public_stops_for_brand (migration 148) for public storefront stops - New server action getPublicStopsForBrand with revalidate=300 + tags - Add revalidateTag invalidation to createStopsBatch + publishStop - Convert /tuxedo/stops and /indian-river-direct/stops to Server Components - Extract TuxedoStopsList + IndianRiverStopsList as client islands (GSAP only) - Removes supabase-js from browser bundle on those routes - Both pages now statically prerendered (5m ISR) Parallel agent changes also staged: - AI provider model list refresh (claude-sonnet-4-5, etc.) - ESLint directive patches for react-hooks/set-state-in-effect - Admin + storefront + checkout + cart updates - New admin_create_stop_rpcs migration (147) - Misc fixes across ~90 files Build verified: typecheck clean, lint clean on new files, production build succeeds.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useCart } from "@/context/CartContext";
|
||||
import { createRetailStripeCheckoutSession } from "@/actions/billing/retail-checkout";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
@@ -134,12 +135,12 @@ export default function CheckoutClient() {
|
||||
<h1 className="text-3xl font-bold text-stone-900">
|
||||
Your cart is empty
|
||||
</h1>
|
||||
<a
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-4 inline-block text-stone-600 hover:text-stone-900"
|
||||
>
|
||||
← Back to storefront
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, Suspense, useState } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { createOrder } from "@/actions/checkout";
|
||||
|
||||
@@ -46,67 +46,68 @@ function formatStopDate(dateStr: string): string {
|
||||
|
||||
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);
|
||||
// Direct access with order_id — load from sessionStorage in lazy initializer.
|
||||
// searchParams values are stable, and sessionStorage is client-only, so this
|
||||
// is safe in a client component.
|
||||
const [order, setOrder] = useState<StoredOrder | null>(() => {
|
||||
if (!orderIdParam || sessionId) return null;
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const stored = sessionStorage.getItem(`order_${orderIdParam}`);
|
||||
if (!stored) return null;
|
||||
return JSON.parse(stored) as StoredOrder;
|
||||
} catch {
|
||||
return 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
|
||||
// Stripe redirected back — create order from pending checkout data.
|
||||
// Wrapped in an async IIFE so all setState calls happen inside a callback,
|
||||
// not in the synchronous effect body (satisfies set-state-in-effect rule).
|
||||
useEffect(() => {
|
||||
if (!sessionId) return;
|
||||
|
||||
const pendingStr = sessionStorage.getItem("pending_checkout");
|
||||
if (!pendingStr) {
|
||||
setError("No pending order found. Please start checkout again.");
|
||||
setCreating(false);
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
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;
|
||||
}
|
||||
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) => {
|
||||
try {
|
||||
const result = await createOrder(
|
||||
pending.idempotencyKey,
|
||||
pending.customerName,
|
||||
pending.customerEmail,
|
||||
pending.customerPhone,
|
||||
pending.stopId,
|
||||
pending.items,
|
||||
pending.cartBrandId,
|
||||
pending.shippingAddress
|
||||
);
|
||||
if (!result.success) {
|
||||
setError(result.error ?? "Failed to create order");
|
||||
setCreating(false);
|
||||
@@ -117,11 +118,11 @@ function SuccessContent() {
|
||||
sessionStorage.removeItem("cart");
|
||||
setOrder(result.order);
|
||||
setCreating(false);
|
||||
})
|
||||
.catch(() => {
|
||||
} catch {
|
||||
setError("Failed to create order. Please contact support.");
|
||||
setCreating(false);
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [sessionId]);
|
||||
|
||||
if (!order || !orderIdParam) {
|
||||
|
||||
Reference in New Issue
Block a user