2cf811a66b
Reviewer caught three runtime breakages that the prior commit would
have introduced (the legacy supabase shim masked them by returning
null):
1. **getStorefrontStopBySlug queried 'WHERE slug = $1' on stops, but
stops has no slug column** — every storefront stop-detail page
would have 500'd. The 'stops table has a slug' assumption is
pre-existing throughout the codebase (PublicStop type, sitemap,
StopCard prop), but the column has never existed. Fix: switch the
route param to the stop's UUID (id). Rename folder [slug] → [id]
for honesty. Update StopCard prop and the two stops-list links to
pass stop.id instead of stop.slug. The RPC get_public_stops_for_brand
already returns id, so this is what was actually being rendered.
2. **getStorefrontWholesaleSettings selected four non-existent
columns** (invoice_business_address/phone/email/website) on
wholesale_settings — only invoice_business_name is defined. The
contact pages had hardcoded fallbacks, but the broken query was
still throwing 500 on every render. Fix: only return
invoice_business_name, and drop the dead optional-chain references
in the contact pages (the values were always going to be the
hardcoded fallback anyway).
3. **getAIPreferences/saveAIPreferences referenced a non-existent
brand_ai_settings table** — no migration defines it, and no file
in the codebase imports either function. Deleted the whole
preferences.ts module (dead code that would have 500'd the moment
anyone wired it up).
4. **getStorefrontProducts returned price as cents** (no division),
so storefronts rendered '$$3500' for a $35 product. Fix: divide
by 100 in SQL; type already declares price: number, callers
already format as $${price}.
Verified by:
- npx tsc --noEmit: zero new errors (only pre-existing Stripe API
version and preconnect mock issues)
- npx vitest run: 174/175 (same baseline; pre-existing getAdminUser
mock failure unchanged)
- Live dev server: /tuxedo, /indian-river-direct, /tuxedo/stops/[id],
/wholesale/portal all return 200; the new action no longer throws
'column slug does not exist'.
162 lines
7.6 KiB
TypeScript
162 lines
7.6 KiB
TypeScript
"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 TuxedoStopsList({ 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="green" />
|
|
|
|
<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-emerald-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 Olathe Sweet™ sweet corn delivered to a stop near you.
|
|
</p>
|
|
<div className="mt-6 h-1 w-12 bg-emerald-600 mx-auto rounded-full" />
|
|
</div>
|
|
|
|
{upcomingStops.length === 0 ? (
|
|
<div className="max-w-2xl 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 Yet</h2>
|
|
<p className="text-stone-500 mb-8 max-w-md mx-auto">
|
|
The corn is still ripening in the field. New pickup stops are added every week — check back soon, or browse the full season schedule.
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
|
<Link
|
|
href="/tuxedo"
|
|
className="inline-flex items-center gap-2 rounded-2xl bg-stone-950 px-6 py-3 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-900 transition-colors"
|
|
>
|
|
Back to Tuxedo Corn
|
|
</Link>
|
|
<a
|
|
href="/api/tuxedo/schedule-pdf"
|
|
download
|
|
className="inline-flex items-center gap-2 rounded-2xl bg-white px-6 py-3 text-sm font-semibold text-stone-700 ring-1 ring-stone-200 hover:bg-stone-50 transition-colors"
|
|
>
|
|
Download Full Schedule
|
|
</a>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="max-w-4xl mx-auto space-y-3">
|
|
{upcomingStops.map((stop) => (
|
|
<Link
|
|
key={stop.id}
|
|
href={`/${brandSlug}/stops/${stop.id}`}
|
|
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-emerald-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-emerald-500 to-emerald-600 flex flex-col items-center justify-center text-white shadow-md shadow-emerald-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-emerald-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-emerald-50 flex items-center justify-center group-hover:bg-emerald-500 transition-colors">
|
|
<svg className="w-5 h-5 text-emerald-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="green" />
|
|
</div>
|
|
);
|
|
}
|