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
+1 -1
View File
@@ -36,7 +36,7 @@ export default function ErrorPage({
<h1 className="text-4xl font-black text-white mb-4">Our Story Unavailable</h1>
<p className="text-blue-100 mb-8 leading-relaxed">
We couldn't load our story page. Please try again.
We couldn&apos;t load our story page. Please try again.
</p>
{process.env.NODE_ENV === "development" && (
@@ -75,7 +75,7 @@ export default function IndianRiverContactPage() {
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Get in Touch</p>
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">Contact Us</h1>
<p className="mt-5 text-lg text-stone-600 max-w-xl mx-auto leading-relaxed">
Questions about citrus, your order, or becoming a wholesale partner? We'd love to hear from you.
Questions about citrus, your order, or becoming a wholesale partner? We&apos;d love to hear from you.
</p>
</motion.div>
@@ -36,7 +36,7 @@ export default function ErrorPage({
<h1 className="text-4xl font-black text-white mb-4">Contact Unavailable</h1>
<p className="text-blue-100 mb-8 leading-relaxed">
We couldn't load the contact page. Please try again.
We couldn&apos;t load the contact page. Please try again.
</p>
{process.env.NODE_ENV === "development" && (
@@ -72,7 +72,7 @@ function FAQAccordion({ items, searchQuery }: { items: FAQItem[]; searchQuery: s
if (filtered.length === 0) {
return (
<div className="rounded-2xl bg-white border-2 border-stone-200 p-8 text-center shadow-lg">
<p className="text-stone-500 text-base">No results for "{searchQuery}"</p>
<p className="text-stone-500 text-base">No results for &quot;{searchQuery}&quot;</p>
<p className="mt-1 text-stone-400 text-sm">Try a different term or browse all categories below.</p>
</div>
);
@@ -208,7 +208,7 @@ export default function IndianRiverFAQPage() {
</div>
<div className="relative">
<p className="text-2xl font-black text-white tracking-tight">Still have questions?</p>
<p className="mt-2 text-blue-100 text-sm">We're happy to help reach out anytime.</p>
<p className="mt-2 text-blue-100 text-sm">We&apos;re happy to help reach out anytime.</p>
<Link
href="/indian-river-direct/contact"
className="mt-8 inline-flex items-center gap-2.5 rounded-full bg-white px-8 py-4 font-bold text-blue-700 hover:bg-blue-50 transition-all text-sm tracking-wider shadow-lg hover:shadow-xl"
+1 -1
View File
@@ -36,7 +36,7 @@ export default function ErrorPage({
<h1 className="text-4xl font-black text-white mb-4">FAQ Unavailable</h1>
<p className="text-blue-100 mb-8 leading-relaxed">
We couldn't load the FAQ page. Please try again.
We couldn&apos;t load the FAQ page. Please try again.
</p>
{process.env.NODE_ENV === "development" && (
+15 -1
View File
@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
@@ -29,6 +29,9 @@ export const metadata: Metadata = {
},
description: "Fresh peaches and citrus from our Florida groves to truckload sales in your neighborhood. Family-owned since 1985. Pre-order now for 2026 season.",
keywords: ["peaches", "citrus", "Florida produce", "truckload sales", "fresh fruit", "Indian River", "wholesale peaches"],
authors: [{ name: "Indian River Direct" }],
creator: "Indian River Direct",
publisher: "Indian River Direct",
openGraph: {
title: "Indian River Direct | Fresh Peaches & Citrus",
description: "Fresh peaches and citrus from our Florida groves to truckload sales in your neighborhood. Family-owned since 1985.",
@@ -50,6 +53,7 @@ export const metadata: Metadata = {
title: "Indian River Direct | Peach & Citrus Truckload",
description: "Fresh peaches and citrus from our Florida groves. Family-owned since 1985. Pre-order for 2026 season.",
site: "@IndianRiverDirect",
creator: "@IndianRiverDirect",
images: ["/og-indian-river.jpg"],
},
alternates: {
@@ -58,12 +62,22 @@ export const metadata: Metadata = {
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
},
},
other: {
"application/ld+json": JSON.stringify(indianRiverBreadcrumbSchema),
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
};
export default function IndianRiverLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen relative">
+1 -1
View File
@@ -435,7 +435,7 @@ export default function IndianRiverDirectPage() {
))}
</div>
<blockquote className="text-stone-600 text-sm leading-relaxed mb-4">
"{t.text}"
&quot;{t.text}&quot;
</blockquote>
<p className="text-stone-950 font-bold text-sm"> {t.name}</p>
</div>
@@ -0,0 +1,144 @@
"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
import { gsap } from "gsap";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
import type { PublicStop } from "@/actions/stops";
type Props = {
stops: PublicStop[];
brandName: string;
brandSlug: string;
};
export default function IndianRiverStopsList({ stops, brandName, brandSlug }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const ctx = gsap.context(() => {
gsap.from(".stop-card", {
y: 30,
opacity: 0,
duration: 0.5,
stagger: 0.08,
ease: "power3.out",
});
}, containerRef);
return () => ctx.revert();
}, []);
const now = new Date();
const upcomingStops = stops.filter((s) => new Date(s.date) >= now);
const pastStops = stops.filter((s) => new Date(s.date) < now);
return (
<div ref={containerRef} className="min-h-screen bg-stone-50">
<StorefrontHeader brandName={brandName} brandSlug={brandSlug} brandAccent="orange" />
<main className="py-16 md:py-20">
<LayoutContainer>
<div className="max-w-4xl mx-auto mb-12 text-center">
<p className="text-[11px] font-bold uppercase tracking-[0.2em] text-orange-600 mb-4">
{brandName}
</p>
<h1 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 mb-4">
Pickup Stops
</h1>
<p className="text-stone-500 text-lg">
Fresh Florida citrus delivered to a stop near you.
</p>
<div className="mt-6 h-1 w-12 bg-orange-500 mx-auto rounded-full" />
</div>
{upcomingStops.length === 0 ? (
<div className="max-w-4xl mx-auto text-center py-16">
<div className="w-20 h-20 rounded-2xl bg-stone-100 flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-stone-800 mb-2">No Upcoming Stops</h2>
<p className="text-stone-500">Check back soon for new pickup locations!</p>
</div>
) : (
<div className="max-w-4xl mx-auto space-y-3">
{upcomingStops.map((stop) => (
<Link
key={stop.id}
href={`/${brandSlug}/stops/${stop.slug}`}
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-orange-200"
>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<div className="flex-shrink-0">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex flex-col items-center justify-center text-white shadow-md shadow-orange-500/20">
<span className="text-[10px] font-bold uppercase opacity-80">
{new Date(stop.date).toLocaleDateString("en-US", { month: "short" })}
</span>
<span className="text-xl font-black leading-none">
{new Date(stop.date).getDate()}
</span>
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-stone-950 group-hover:text-orange-700 transition-colors">
{stop.city}, {stop.state}
</h3>
<p className="text-sm text-stone-500 truncate">{stop.location}</p>
{stop.address && (
<p className="text-xs text-stone-400 truncate">{stop.address}</p>
)}
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-semibold text-stone-700">{stop.time}</p>
{stop.cutoff_time && (
<p className="text-xs text-stone-400">Order by {new Date(stop.cutoff_time).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}</p>
)}
</div>
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center group-hover:bg-orange-500 transition-colors">
<svg className="w-5 h-5 text-orange-600 group-hover:text-white transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</Link>
))}
{pastStops.length > 0 && (
<div className="mt-8 pt-8 border-t border-stone-200">
<h2 className="text-sm font-bold uppercase tracking-widest text-stone-400 mb-4">Past Stops</h2>
<div className="space-y-2 opacity-60">
{pastStops.slice(0, 5).map((stop) => (
<div
key={stop.id}
className="flex items-center gap-4 p-4 rounded-xl bg-white/50 text-stone-500"
>
<div className="w-10 h-10 rounded-lg bg-stone-200 flex items-center justify-center text-xs font-bold text-stone-500">
{new Date(stop.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
</div>
<div className="flex-1">
<p className="font-medium">{stop.city}, {stop.state}</p>
<p className="text-xs">{stop.location}</p>
</div>
<span className="text-xs">Completed</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</LayoutContainer>
</main>
<StorefrontFooter brandName={brandName} brandSlug={brandSlug} brandAccent="orange" />
</div>
);
}
@@ -36,7 +36,7 @@ export default function ErrorPage({
<h1 className="text-4xl font-black text-white mb-4">Stop Not Available</h1>
<p className="text-blue-100 mb-8 leading-relaxed">
We couldn't load this stop. Please try again.
We couldn&apos;t load this stop. Please try again.
</p>
{process.env.NODE_ENV === "development" && (
+18 -178
View File
@@ -1,184 +1,24 @@
"use client";
import { getPublicStopsForBrand } from "@/actions/stops";
import { getBrandSettingsPublic } from "@/actions/brand-settings";
import IndianRiverStopsList from "./IndianRiverStopsList";
import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import { gsap } from "gsap";
import { supabase } from "@/lib/supabase";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
import { formatDate } from "@/lib/format-date";
// Page-level cache — matches the 5-min revalidate in getPublicStopsForBrand.
// Mutations call revalidateTag("stops") to invalidate.
export const revalidate = 300;
type Stop = {
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
address: string | null;
slug: string;
cutoff_time: string | null;
};
export default async function IndianRiverStopsPage() {
const [stops, settings] = await Promise.all([
getPublicStopsForBrand("indian-river-direct"),
getBrandSettingsPublic("indian-river-direct"),
]);
const BRAND_NAME = "Indian River Direct";
const BRAND_SLUG = "indian-river-direct";
const BRAND_ACCENT = "blue";
const BRAND_ID = "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28";
export default function IndianRiverStopsPage() {
const containerRef = useRef<HTMLDivElement>(null);
const [stops, setStops] = useState<Stop[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadStops() {
const { data } = await supabase
.from("stops")
.select("id, city, state, date, time, location, address, slug, cutoff_time")
.eq("active", true)
.eq("brand_id", BRAND_ID)
.order("date", { ascending: true });
setStops(data ?? []);
setLoading(false);
}
loadStops();
// GSAP animations
if (typeof window !== "undefined" && containerRef.current) {
const ctx = gsap.context(() => {
gsap.from(".stop-card", {
y: 30,
opacity: 0,
duration: 0.5,
stagger: 0.08,
ease: "power3.out",
});
}, containerRef);
return () => ctx.revert();
}
}, []);
const now = new Date();
const upcomingStops = stops.filter(s => new Date(s.date) >= now);
const pastStops = stops.filter(s => new Date(s.date) < now);
const brandName = settings.success ? settings.settings?.brand_name : null;
return (
<div ref={containerRef} className="min-h-screen bg-stone-50">
<StorefrontHeader brandName={BRAND_NAME} brandSlug={BRAND_SLUG} brandAccent={BRAND_ACCENT as "green" | "orange" | "blue"} />
<main className="py-16 md:py-20">
<LayoutContainer>
{/* Header */}
<div className="max-w-4xl mx-auto mb-12 text-center">
<p className="text-[11px] font-bold uppercase tracking-[0.2em] text-orange-600 mb-4">
{BRAND_NAME}
</p>
<h1 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 mb-4">
Pickup Stops
</h1>
<p className="text-stone-500 text-lg">
Fresh Florida citrus delivered to a stop near you.
</p>
<div className="mt-6 h-1 w-12 bg-orange-500 mx-auto rounded-full" />
</div>
{loading ? (
<div className="max-w-4xl mx-auto space-y-4">
{[1, 2, 3, 4].map(i => (
<div key={i} className="h-24 bg-stone-200 rounded-2xl animate-pulse" />
))}
</div>
) : upcomingStops.length === 0 ? (
<div className="max-w-4xl mx-auto text-center py-16">
<div className="w-20 h-20 rounded-2xl bg-stone-100 flex items-center justify-center mx-auto mb-6">
<svg className="w-10 h-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-stone-800 mb-2">No Upcoming Stops</h2>
<p className="text-stone-500">Check back soon for new pickup locations!</p>
</div>
) : (
<div className="max-w-4xl mx-auto space-y-3">
{upcomingStops.map((stop) => (
<Link
key={stop.id}
href={`/indian-river-direct/stops/${stop.slug}`}
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-orange-200"
>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
{/* Date badge */}
<div className="flex-shrink-0">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex flex-col items-center justify-center text-white shadow-md shadow-orange-500/20">
<span className="text-[10px] font-bold uppercase opacity-80">
{new Date(stop.date).toLocaleDateString('en-US', { month: 'short' })}
</span>
<span className="text-xl font-black leading-none">
{new Date(stop.date).getDate()}
</span>
</div>
</div>
{/* Stop info */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-bold text-stone-950 group-hover:text-orange-700 transition-colors">
{stop.city}, {stop.state}
</h3>
<p className="text-sm text-stone-500 truncate">{stop.location}</p>
{stop.address && (
<p className="text-xs text-stone-400 truncate">{stop.address}</p>
)}
</div>
{/* Time & CTA */}
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-semibold text-stone-700">{stop.time}</p>
{stop.cutoff_time && (
<p className="text-xs text-stone-400">Order by {stop.cutoff_time}</p>
)}
</div>
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center group-hover:bg-orange-500 transition-colors">
<svg className="w-5 h-5 text-orange-600 group-hover:text-white transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
</div>
</Link>
))}
{/* Past stops */}
{pastStops.length > 0 && (
<div className="mt-8 pt-8 border-t border-stone-200">
<h2 className="text-sm font-bold uppercase tracking-widest text-stone-400 mb-4">Past Stops</h2>
<div className="space-y-2 opacity-60">
{pastStops.slice(0, 5).map(stop => (
<div
key={stop.id}
className="flex items-center gap-4 p-4 rounded-xl bg-white/50 text-stone-500"
>
<div className="w-10 h-10 rounded-lg bg-stone-200 flex items-center justify-center text-xs font-bold text-stone-500">
{new Date(stop.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</div>
<div className="flex-1">
<p className="font-medium">{stop.city}, {stop.state}</p>
<p className="text-xs">{stop.location}</p>
</div>
<span className="text-xs">Completed</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</LayoutContainer>
</main>
<StorefrontFooter brandName={BRAND_NAME} brandSlug={BRAND_SLUG} brandAccent={BRAND_ACCENT as "green" | "orange" | "blue"} />
</div>
<IndianRiverStopsList
stops={stops}
brandName={brandName ?? "Indian River Direct"}
brandSlug="indian-river-direct"
/>
);
}
}