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:
2026-06-03 02:04:21 +00:00
parent 57da01c786
commit 1fe5ffee8d
95 changed files with 1470 additions and 733 deletions
+3 -2
View File
@@ -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>
+56 -55
View File
@@ -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) {