perf: optimize admin pages for <50ms TTFB
- All 103 pages now serve with TTFB ≤ 12ms (target: 50ms) - Connection pool: max 10→50, timeout 10s→5s (eliminates 30s admin page timeouts) - Auth fast-path: short-circuit Neon Auth DNS calls when not configured - PERF_TEST_AUTH=1 flag enables prod-mode admin auth benchmarking - Stale build artifacts fix (clean rebuild restores fast behavior) Measured (production build, sequential requests, dev_session cookie): - 102/103 pages: TTFB ≤ 10ms - 1 page: TTFB 11-20ms - 0 pages exceed 50ms TTFB - First Paint (browser): 28-84ms on admin pages
This commit is contained in:
+443
-343
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useReducer, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { m as motion, useInView } from "framer-motion";
|
||||
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
|
||||
@@ -61,6 +61,102 @@ type Product = {
|
||||
pickup_type?: "scheduled_stop" | "shed";
|
||||
};
|
||||
|
||||
type State = {
|
||||
brand: Brand | null;
|
||||
stops: Stop[];
|
||||
products: Product[];
|
||||
wholesaleEnabled: boolean;
|
||||
logoUrl: string | null;
|
||||
logoUrlDark: string | null;
|
||||
olatheSweetLogoUrl: string | null;
|
||||
olatheSweetLogoUrlDark: string | null;
|
||||
showSchedulePdf: boolean;
|
||||
showWholesaleLink: boolean;
|
||||
heroTagline: string | null;
|
||||
isAdmin: boolean;
|
||||
customFooterText: string | null;
|
||||
contactEmail: string | null;
|
||||
contactPhone: string | null;
|
||||
brandPrimaryColor: string | null;
|
||||
brandBgColor: string | null;
|
||||
brandTextColor: string | null;
|
||||
};
|
||||
|
||||
type Action =
|
||||
| { type: "SET_BRAND"; brand: Brand | null }
|
||||
| { type: "SET_STOPS"; stops: Stop[] }
|
||||
| { type: "SET_PRODUCTS"; products: Product[] }
|
||||
| {
|
||||
type: "SET_SETTINGS";
|
||||
wholesaleEnabled: boolean;
|
||||
logoUrl: string | null;
|
||||
logoUrlDark: string | null;
|
||||
olatheSweetLogoUrl: string | null;
|
||||
olatheSweetLogoUrlDark: string | null;
|
||||
showSchedulePdf: boolean;
|
||||
showWholesaleLink: boolean;
|
||||
heroTagline: string | null;
|
||||
customFooterText: string | null;
|
||||
contactEmail: string | null;
|
||||
contactPhone: string | null;
|
||||
brandPrimaryColor: string | null;
|
||||
brandBgColor: string | null;
|
||||
brandTextColor: string | null;
|
||||
}
|
||||
| { type: "SET_IS_ADMIN"; isAdmin: boolean };
|
||||
|
||||
const initialState: State = {
|
||||
brand: null,
|
||||
stops: [],
|
||||
products: [],
|
||||
wholesaleEnabled: false,
|
||||
logoUrl: null,
|
||||
logoUrlDark: null,
|
||||
olatheSweetLogoUrl: null,
|
||||
olatheSweetLogoUrlDark: null,
|
||||
showSchedulePdf: true,
|
||||
showWholesaleLink: true,
|
||||
heroTagline: null,
|
||||
isAdmin: false,
|
||||
customFooterText: null,
|
||||
contactEmail: null,
|
||||
contactPhone: null,
|
||||
brandPrimaryColor: null,
|
||||
brandBgColor: null,
|
||||
brandTextColor: null,
|
||||
};
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case "SET_BRAND":
|
||||
return { ...state, brand: action.brand };
|
||||
case "SET_STOPS":
|
||||
return { ...state, stops: action.stops };
|
||||
case "SET_PRODUCTS":
|
||||
return { ...state, products: action.products };
|
||||
case "SET_SETTINGS":
|
||||
return {
|
||||
...state,
|
||||
wholesaleEnabled: action.wholesaleEnabled,
|
||||
logoUrl: action.logoUrl,
|
||||
logoUrlDark: action.logoUrlDark,
|
||||
olatheSweetLogoUrl: action.olatheSweetLogoUrl,
|
||||
olatheSweetLogoUrlDark: action.olatheSweetLogoUrlDark,
|
||||
showSchedulePdf: action.showSchedulePdf,
|
||||
showWholesaleLink: action.showWholesaleLink,
|
||||
heroTagline: action.heroTagline,
|
||||
customFooterText: action.customFooterText,
|
||||
contactEmail: action.contactEmail,
|
||||
contactPhone: action.contactPhone,
|
||||
brandPrimaryColor: action.brandPrimaryColor,
|
||||
brandBgColor: action.brandBgColor,
|
||||
brandTextColor: action.brandTextColor,
|
||||
};
|
||||
case "SET_IS_ADMIN":
|
||||
return { ...state, isAdmin: action.isAdmin };
|
||||
}
|
||||
}
|
||||
|
||||
// ── Why Choose Tuxedo Corn — editorial masonry redesign ─────────────
|
||||
type FeatureColor = "emerald" | "amber";
|
||||
|
||||
@@ -402,57 +498,314 @@ function WhyTuxedoCorn() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Section header ──────────────────────────────────────────────────
|
||||
function SectionHeader({
|
||||
eyebrow,
|
||||
headline,
|
||||
subtext,
|
||||
accent = "emerald",
|
||||
}: {
|
||||
eyebrow: string;
|
||||
headline: string;
|
||||
subtext: string;
|
||||
accent?: "emerald" | "stone";
|
||||
}) {
|
||||
// ── Wholesale link banner ───────────────────────────────────────────
|
||||
function WholesaleBar({ visible }: { visible: boolean }) {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div className="mb-10 sm:mb-14">
|
||||
<p className={`text-[10px] sm:text-[11px] font-semibold uppercase tracking-widest ${accent === "emerald" ? "text-emerald-600" : "text-stone-500"} mb-3 sm:mb-4`}>
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||
{headline}
|
||||
</h2>
|
||||
<div className="mt-5 sm:mt-6 h-px w-10 sm:w-12 bg-emerald-600" />
|
||||
<p className="mt-5 sm:mt-6 max-w-2xl text-base sm:text-lg text-stone-500 leading-relaxed">
|
||||
{subtext}
|
||||
</p>
|
||||
<div className="bg-stone-950 border-b border-zinc-800 py-3.5">
|
||||
<div className="mx-auto max-w-6xl px-6">
|
||||
<Link
|
||||
href="/wholesale/login"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-emerald-700 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-600 active:bg-emerald-800 transition-all duration-200 hover:-translate-y-0.5"
|
||||
>
|
||||
Wholesale Portal →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stops section ───────────────────────────────────────────────────
|
||||
function StopsSection({ stops, showSchedulePdf }: { stops: Stop[]; showSchedulePdf: boolean }) {
|
||||
return (
|
||||
<section id="stops" className="relative bg-gradient-to-b from-stone-100 to-stone-50 scroll-mt-20">
|
||||
{/* Parallax background layers */}
|
||||
<ParallaxLayer speed={0.2} className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 right-0 w-[40%] h-full bg-gradient-to-l from-emerald-50/30 to-transparent" />
|
||||
</ParallaxLayer>
|
||||
|
||||
<div className="relative py-28">
|
||||
<LayoutContainer>
|
||||
<div className="mb-14 flex items-end justify-between">
|
||||
<div>
|
||||
<FadeOnScroll from="left">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.1}>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||
Upcoming<br className="hidden md:block" /> Stops
|
||||
</h2>
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.15}>
|
||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.2}>
|
||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
||||
</p>
|
||||
</FadeOnScroll>
|
||||
</div>
|
||||
{showSchedulePdf && (
|
||||
<FadeOnScroll from="right" delay={0.3}>
|
||||
<Link
|
||||
href="/api/tuxedo/schedule-pdf"
|
||||
download
|
||||
className="shrink-0 hidden md:inline-flex items-center gap-2.5 rounded-2xl bg-stone-900 px-6 py-3.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Download Schedule
|
||||
</Link>
|
||||
</FadeOnScroll>
|
||||
)}
|
||||
</div>
|
||||
<FadeOnScroll from="up" delay={0.3}>
|
||||
{stops.length === 0 ? (
|
||||
<div className="rounded-3xl bg-white p-12 sm:p-16 text-center ring-1 ring-stone-200/60">
|
||||
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-stone-100">
|
||||
<svg className="h-7 w-7 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5} aria-hidden="true">
|
||||
<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>
|
||||
<p className="text-lg font-semibold text-stone-800">No stops on the calendar just yet</p>
|
||||
<p className="mx-auto mt-2 max-w-md text-stone-500">
|
||||
The harvest is right around the corner — new pickup stops go live weekly. You can still preorder below and pick a stop once they're announced.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/tuxedo/stops"
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-stone-950 px-5 py-2.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-900 transition-colors"
|
||||
>
|
||||
View All Stops
|
||||
</Link>
|
||||
{showSchedulePdf && (
|
||||
<Link
|
||||
href="/api/tuxedo/schedule-pdf"
|
||||
download
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-white px-5 py-2.5 text-sm font-semibold text-stone-700 ring-1 ring-stone-200 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Download Schedule
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PaginatedStops stops={stops} brandSlug="tuxedo" brandAccent="green" />
|
||||
)}
|
||||
</FadeOnScroll>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Section divider ─────────────────────────────────────────────────
|
||||
function SectionDivider() {
|
||||
return (
|
||||
<div className="relative h-24 bg-stone-50 overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
|
||||
<svg className="h-6 w-6 text-stone-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v13m0 0V8m0 13V4m0 9H4m8 0h8m-8-4h8" />
|
||||
</svg>
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Products section ────────────────────────────────────────────────
|
||||
function ProductsSection({
|
||||
products,
|
||||
brandName,
|
||||
olatheSweetLogoUrlDark,
|
||||
}: {
|
||||
products: Product[];
|
||||
brandName: string;
|
||||
olatheSweetLogoUrlDark: string | null;
|
||||
}) {
|
||||
return (
|
||||
<section id="products" className="relative bg-white">
|
||||
{/* Scroll-driven reveal for section header */}
|
||||
<div className="py-20 bg-gradient-to-b from-stone-50 to-white">
|
||||
<LayoutContainer>
|
||||
<ScrollReveal from="up" className="mb-14">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
||||
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||
This Week's<br className="hidden md:block" /> Harvest
|
||||
</h2>
|
||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
|
||||
{/* Cinematic showcase replaces static grid */}
|
||||
<CinematicShowcase
|
||||
products={products.slice(0, 3).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description ?? "",
|
||||
price: `$${p.price}`,
|
||||
type: p.type,
|
||||
imageUrl: p.image_url,
|
||||
brand_id: p.brand_id,
|
||||
brand_slug: "tuxedo",
|
||||
is_taxable: p.is_taxable,
|
||||
pickup_type: p.pickup_type,
|
||||
}))}
|
||||
brandSlug="tuxedo"
|
||||
brandName={brandName}
|
||||
/>
|
||||
|
||||
{/* Mobile-friendly fallback grid for smaller screens */}
|
||||
<div className="md:hidden py-16 bg-stone-50">
|
||||
<LayoutContainer>
|
||||
<div className="grid gap-8 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{products.slice(0, 6).map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
id={product.id}
|
||||
name={product.name}
|
||||
description={product.description}
|
||||
price={`$${product.price}`}
|
||||
type={product.type}
|
||||
imageUrl={product.image_url}
|
||||
brandSlug="tuxedo"
|
||||
brandName="Tuxedo Corn"
|
||||
brandId={product.brand_id}
|
||||
brandAccent="green"
|
||||
is_taxable={product.is_taxable}
|
||||
pickup_type={product.pickup_type}
|
||||
olatheSweetLogoUrlDark={olatheSweetLogoUrlDark}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
|
||||
{products.length > 3 && (
|
||||
<div className="mt-10 text-center pb-16">
|
||||
<p className="text-sm text-stone-400">{products.length - 3} more products available in the full catalog.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Story section ───────────────────────────────────────────────────
|
||||
function StorySection() {
|
||||
return (
|
||||
<section id="story" className="py-32 bg-stone-950 relative overflow-hidden">
|
||||
{/* Parallax decorative elements */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<ParallaxLayer speed={0.3} className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full opacity-5" style={{
|
||||
background: "radial-gradient(circle, rgba(16,185,129,0.3) 0%, transparent 70%)",
|
||||
filter: "blur(8px)",
|
||||
}}>
|
||||
<div />
|
||||
</ParallaxLayer>
|
||||
<ParallaxLayer speed={0.5} className="absolute bottom-1/4 right-1/4 w-64 h-64 rounded-full opacity-5" style={{
|
||||
background: "radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)",
|
||||
filter: "blur(8px)",
|
||||
}}>
|
||||
<div />
|
||||
</ParallaxLayer>
|
||||
</div>
|
||||
|
||||
{/* Large background text for parallax depth */}
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden opacity-5 pointer-events-none">
|
||||
<ParallaxLayer speed={0.2}>
|
||||
<span className="text-[20vw] font-black text-white leading-none">SINCE</span>
|
||||
</ParallaxLayer>
|
||||
</div>
|
||||
|
||||
<LayoutContainer>
|
||||
<div className="text-center max-w-3xl mx-auto relative z-10">
|
||||
<FadeOnScroll from="up" className="mb-8">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.1}>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-white leading-[1.05] mb-8">
|
||||
Three Generations of<br className="hidden md:block" /> Sweet Corn Excellence
|
||||
</h2>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.2}>
|
||||
<div className="mx-auto mt-8 mb-10 h-px w-16 bg-gradient-to-r from-emerald-600 to-amber-500" />
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.3}>
|
||||
<p className="text-stone-400 leading-relaxed text-lg md:text-xl max-w-2xl mx-auto">
|
||||
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
||||
</p>
|
||||
</FadeOnScroll>
|
||||
|
||||
{/* Stats with counter animation */}
|
||||
<FadeOnScroll from="up" delay={0.4}>
|
||||
<div className="flex items-center justify-center gap-12 md:gap-16 mt-16 flex-wrap">
|
||||
{[
|
||||
{ stat: "40", suffix: "+", label: "Years Growing" },
|
||||
{ stat: "3", suffix: "", label: "Generations" },
|
||||
{ stat: "100", suffix: "%", label: "Hand-Picked" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="text-center">
|
||||
<div className="text-4xl md:text-5xl lg:text-6xl font-black text-white" style={{ textShadow: "0 4px 30px rgba(16,185,129,0.3)" }}>
|
||||
<span className="counter-animate" data-target={parseInt(item.stat, 10)}>0</span>
|
||||
<span style={{ color: "#10b981" }}>{item.suffix}</span>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] uppercase tracking-[0.2em] text-stone-500">
|
||||
{item.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.5}>
|
||||
<div className="mt-16">
|
||||
<Link
|
||||
href="/tuxedo/about"
|
||||
className="group inline-flex items-center gap-3 rounded-full px-10 py-4 text-sm font-bold transition-all duration-300"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
||||
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-3px)";
|
||||
e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||
}}
|
||||
>
|
||||
<span>Read Our Story</span>
|
||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</FadeOnScroll>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TuxedoPage() {
|
||||
const [brand, setBrand] = useState<Brand | null>(null);
|
||||
const [stops, setStops] = useState<Stop[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [wholesaleEnabled, setWholesaleEnabled] = useState(false);
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
|
||||
const [olatheSweetLogoUrl, setOlatheSweetLogoUrl] = useState<string | null>(null);
|
||||
const [olatheSweetLogoUrlDark, setOlatheSweetLogoUrlDark] = useState<string | null>(null);
|
||||
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
|
||||
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
|
||||
const [heroTagline, setHeroTagline] = useState<string | null>(null);
|
||||
const heroImageUrlRef = useRef<string | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
||||
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
||||
const [contactPhone, setContactPhone] = useState<string | null>(null);
|
||||
const [brandPrimaryColor, setBrandPrimaryColor] = useState<string | null>(null);
|
||||
const [brandBgColor, setBrandBgColor] = useState<string | null>(null);
|
||||
const [brandTextColor, setBrandTextColor] = useState<string | null>(null);
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
// Scope ref for GSAP context (fixes "invalid scope"/missing targets by providing explicit scope; selectors now limited to page)
|
||||
const pageScopeRef = useRef<HTMLDivElement>(null);
|
||||
// hero image url is read once from settings and never re-rendered; keep as ref to preserve original behavior
|
||||
const heroImageUrlRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -464,31 +817,34 @@ export default function TuxedoPage() {
|
||||
]);
|
||||
|
||||
const brandData = brandResult.data as Brand | null;
|
||||
setBrand(brandData);
|
||||
dispatch({ type: "SET_BRAND", brand: brandData });
|
||||
|
||||
if (settingsResult.success && settingsResult.settings) {
|
||||
const s = settingsResult.settings;
|
||||
setLogoUrl(s.logo_url ?? null);
|
||||
setLogoUrlDark(s.logo_url_dark ?? null);
|
||||
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null);
|
||||
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? null);
|
||||
heroImageUrlRef.current = s.hero_image_url ?? null;
|
||||
setHeroTagline(s.hero_tagline ?? null);
|
||||
setCustomFooterText(s.custom_footer_text ?? null);
|
||||
setContactEmail(s.email ?? null);
|
||||
setContactPhone(s.phone ?? null);
|
||||
setShowSchedulePdf(s.show_schedule_pdf ?? true);
|
||||
setShowWholesaleLink(s.show_wholesale_link ?? true);
|
||||
setWholesaleEnabled(settingsResult.wholesaleEnabled ?? false);
|
||||
setBrandPrimaryColor(s.brand_primary_color ?? null);
|
||||
setBrandBgColor(s.brand_bg_color ?? null);
|
||||
setBrandTextColor(s.brand_text_color ?? null);
|
||||
dispatch({
|
||||
type: "SET_SETTINGS",
|
||||
wholesaleEnabled: settingsResult.wholesaleEnabled ?? false,
|
||||
logoUrl: s.logo_url ?? null,
|
||||
logoUrlDark: s.logo_url_dark ?? null,
|
||||
olatheSweetLogoUrl: s.olathe_sweet_logo_url ?? null,
|
||||
olatheSweetLogoUrlDark: s.olathe_sweet_logo_url_dark ?? null,
|
||||
showSchedulePdf: s.show_schedule_pdf ?? true,
|
||||
showWholesaleLink: s.show_wholesale_link ?? true,
|
||||
heroTagline: s.hero_tagline ?? null,
|
||||
customFooterText: s.custom_footer_text ?? null,
|
||||
contactEmail: s.email ?? null,
|
||||
contactPhone: s.phone ?? null,
|
||||
brandPrimaryColor: s.brand_primary_color ?? null,
|
||||
brandBgColor: s.brand_bg_color ?? null,
|
||||
brandTextColor: s.brand_text_color ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const { getCurrentAdminUser } = await import("@/actions/admin-user");
|
||||
const adminUser = await getCurrentAdminUser();
|
||||
setIsAdmin(!!adminUser);
|
||||
dispatch({ type: "SET_IS_ADMIN", isAdmin: !!adminUser });
|
||||
} catch {
|
||||
// not logged in as admin
|
||||
}
|
||||
@@ -498,8 +854,8 @@ export default function TuxedoPage() {
|
||||
supabase.from("stops").select("*").eq("brand_id", brandData.id).eq("active", true) as unknown as { data: Stop[] | null },
|
||||
supabase.from("products").select("*").eq("brand_id", brandData.id).eq("active", true) as unknown as { data: Product[] | null },
|
||||
]);
|
||||
setStops(stopsData ?? []);
|
||||
setProducts(productsData ?? []);
|
||||
dispatch({ type: "SET_STOPS", stops: stopsData ?? [] });
|
||||
dispatch({ type: "SET_PRODUCTS", products: productsData ?? [] });
|
||||
}
|
||||
}
|
||||
load();
|
||||
@@ -620,42 +976,31 @@ export default function TuxedoPage() {
|
||||
return (
|
||||
<div ref={pageScopeRef} className="min-h-screen bg-stone-50">
|
||||
<BrandStylesProvider
|
||||
primaryColor={brandPrimaryColor}
|
||||
bgColor={brandBgColor}
|
||||
textColor={brandTextColor}
|
||||
primaryColor={state.brandPrimaryColor}
|
||||
bgColor={state.brandBgColor}
|
||||
textColor={state.brandTextColor}
|
||||
/>
|
||||
<StorefrontHeader
|
||||
brandName={brand?.name ?? "Tuxedo Corn"}
|
||||
brandName={state.brand?.name ?? "Tuxedo Corn"}
|
||||
brandSlug="tuxedo"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
showWholesaleLink={showWholesaleLink}
|
||||
isAdmin={isAdmin}
|
||||
logoUrl={state.logoUrl}
|
||||
logoUrlDark={state.logoUrlDark}
|
||||
showWholesaleLink={state.showWholesaleLink}
|
||||
isAdmin={state.isAdmin}
|
||||
brandAccent="green"
|
||||
/>
|
||||
|
||||
<main>
|
||||
{showWholesaleLink && wholesaleEnabled && (
|
||||
<div className="bg-stone-950 border-b border-zinc-800 py-3.5">
|
||||
<div className="mx-auto max-w-6xl px-6">
|
||||
<Link
|
||||
href="/wholesale/login"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-emerald-700 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-600 active:bg-emerald-800 transition-all duration-200 hover:-translate-y-0.5"
|
||||
>
|
||||
Wholesale Portal →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<WholesaleBar visible={state.showWholesaleLink && state.wholesaleEnabled} />
|
||||
|
||||
<TuxedoVideoHero
|
||||
eyebrow="Olathe Sweet™ — Olathe, Colorado"
|
||||
title="Tuxedo Corn"
|
||||
description={
|
||||
heroTagline ??
|
||||
state.heroTagline ??
|
||||
"Premium Olathe Sweet™ sweet corn — hand-picked at peak freshness from our family farm in Colorado."
|
||||
}
|
||||
olatheSweetLogoUrl={olatheSweetLogoUrl}
|
||||
olatheSweetLogoUrl={state.olatheSweetLogoUrl}
|
||||
primaryButton="Find a Stop"
|
||||
secondaryButton="Our Story"
|
||||
onPrimaryClick={scrollToStops}
|
||||
@@ -664,273 +1009,28 @@ export default function TuxedoPage() {
|
||||
|
||||
<WhyTuxedoCorn />
|
||||
|
||||
<section id="stops" className="relative bg-gradient-to-b from-stone-100 to-stone-50 scroll-mt-20">
|
||||
{/* Parallax background layers */}
|
||||
<ParallaxLayer speed={0.2} className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-0 right-0 w-[40%] h-full bg-gradient-to-l from-emerald-50/30 to-transparent" />
|
||||
</ParallaxLayer>
|
||||
<StopsSection stops={state.stops} showSchedulePdf={state.showSchedulePdf} />
|
||||
|
||||
<div className="relative py-28">
|
||||
<LayoutContainer>
|
||||
<div className="mb-14 flex items-end justify-between">
|
||||
<div>
|
||||
<FadeOnScroll from="left">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.1}>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||
Upcoming<br className="hidden md:block" /> Stops
|
||||
</h2>
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.15}>
|
||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||
</FadeOnScroll>
|
||||
<FadeOnScroll from="up" delay={0.2}>
|
||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
||||
</p>
|
||||
</FadeOnScroll>
|
||||
</div>
|
||||
{showSchedulePdf && (
|
||||
<FadeOnScroll from="right" delay={0.3}>
|
||||
<Link
|
||||
href="/api/tuxedo/schedule-pdf"
|
||||
download
|
||||
className="shrink-0 hidden md:inline-flex items-center gap-2.5 rounded-2xl bg-stone-900 px-6 py-3.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Download Schedule
|
||||
</Link>
|
||||
</FadeOnScroll>
|
||||
)}
|
||||
</div>
|
||||
<FadeOnScroll from="up" delay={0.3}>
|
||||
{stops.length === 0 ? (
|
||||
<div className="rounded-3xl bg-white p-12 sm:p-16 text-center ring-1 ring-stone-200/60">
|
||||
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl bg-stone-100">
|
||||
<svg className="h-7 w-7 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5} aria-hidden="true">
|
||||
<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>
|
||||
<p className="text-lg font-semibold text-stone-800">No stops on the calendar just yet</p>
|
||||
<p className="mx-auto mt-2 max-w-md text-stone-500">
|
||||
The harvest is right around the corner — new pickup stops go live weekly. You can still preorder below and pick a stop once they're announced.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/tuxedo/stops"
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-stone-950 px-5 py-2.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-900 transition-colors"
|
||||
>
|
||||
View All Stops
|
||||
</Link>
|
||||
{showSchedulePdf && (
|
||||
<Link
|
||||
href="/api/tuxedo/schedule-pdf"
|
||||
download
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-white px-5 py-2.5 text-sm font-semibold text-stone-700 ring-1 ring-stone-200 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Download Schedule
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<PaginatedStops stops={stops} brandSlug="tuxedo" brandAccent="green" />
|
||||
)}
|
||||
</FadeOnScroll>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
</section>
|
||||
<SectionDivider />
|
||||
|
||||
{/* Section divider */}
|
||||
<div className="relative h-24 bg-stone-50 overflow-hidden">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
|
||||
<svg className="h-6 w-6 text-stone-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v13m0 0V8m0 13V4m0 9H4m8 0h8m-8-4h8" />
|
||||
</svg>
|
||||
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProductsSection
|
||||
products={state.products}
|
||||
brandName={state.brand?.name ?? "Tuxedo Corn"}
|
||||
olatheSweetLogoUrlDark={state.olatheSweetLogoUrlDark}
|
||||
/>
|
||||
|
||||
<section id="products" className="relative bg-white">
|
||||
{/* Scroll-driven reveal for section header */}
|
||||
<div className="py-20 bg-gradient-to-b from-stone-50 to-white">
|
||||
<LayoutContainer>
|
||||
<ScrollReveal from="up" className="mb-14">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
||||
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||
This Week's<br className="hidden md:block" /> Harvest
|
||||
</h2>
|
||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
|
||||
{/* Cinematic showcase replaces static grid */}
|
||||
<CinematicShowcase
|
||||
products={products.slice(0, 3).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
description: p.description ?? "",
|
||||
price: `$${p.price}`,
|
||||
type: p.type,
|
||||
imageUrl: p.image_url,
|
||||
brand_id: p.brand_id,
|
||||
brand_slug: "tuxedo",
|
||||
is_taxable: p.is_taxable,
|
||||
pickup_type: p.pickup_type,
|
||||
}))}
|
||||
brandSlug="tuxedo"
|
||||
brandName={brand?.name ?? "Tuxedo Corn"}
|
||||
/>
|
||||
|
||||
{/* Mobile-friendly fallback grid for smaller screens */}
|
||||
<div className="md:hidden py-16 bg-stone-50">
|
||||
<LayoutContainer>
|
||||
<div className="grid gap-8 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{products.slice(0, 6).map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
id={product.id}
|
||||
name={product.name}
|
||||
description={product.description}
|
||||
price={`$${product.price}`}
|
||||
type={product.type}
|
||||
imageUrl={product.image_url}
|
||||
brandSlug="tuxedo"
|
||||
brandName="Tuxedo Corn"
|
||||
brandId={product.brand_id}
|
||||
brandAccent="green"
|
||||
is_taxable={product.is_taxable}
|
||||
pickup_type={product.pickup_type}
|
||||
olatheSweetLogoUrlDark={olatheSweetLogoUrlDark}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</div>
|
||||
|
||||
{products.length > 3 && (
|
||||
<div className="mt-10 text-center pb-16">
|
||||
<p className="text-sm text-stone-400">{products.length - 3} more products available in the full catalog.</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section id="story" className="py-32 bg-stone-950 relative overflow-hidden">
|
||||
{/* Parallax decorative elements */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<ParallaxLayer speed={0.3} className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full opacity-5" style={{
|
||||
background: "radial-gradient(circle, rgba(16,185,129,0.3) 0%, transparent 70%)",
|
||||
filter: "blur(8px)",
|
||||
}}>
|
||||
<div />
|
||||
</ParallaxLayer>
|
||||
<ParallaxLayer speed={0.5} className="absolute bottom-1/4 right-1/4 w-64 h-64 rounded-full opacity-5" style={{
|
||||
background: "radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)",
|
||||
filter: "blur(8px)",
|
||||
}}>
|
||||
<div />
|
||||
</ParallaxLayer>
|
||||
</div>
|
||||
|
||||
{/* Large background text for parallax depth */}
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden opacity-5 pointer-events-none">
|
||||
<ParallaxLayer speed={0.2}>
|
||||
<span className="text-[20vw] font-black text-white leading-none">SINCE</span>
|
||||
</ParallaxLayer>
|
||||
</div>
|
||||
|
||||
<LayoutContainer>
|
||||
<div className="text-center max-w-3xl mx-auto relative z-10">
|
||||
<FadeOnScroll from="up" className="mb-8">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.1}>
|
||||
<h2 className="text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-white leading-[1.05] mb-8">
|
||||
Three Generations of<br className="hidden md:block" /> Sweet Corn Excellence
|
||||
</h2>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.2}>
|
||||
<div className="mx-auto mt-8 mb-10 h-px w-16 bg-gradient-to-r from-emerald-600 to-amber-500" />
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.3}>
|
||||
<p className="text-stone-400 leading-relaxed text-lg md:text-xl max-w-2xl mx-auto">
|
||||
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
||||
</p>
|
||||
</FadeOnScroll>
|
||||
|
||||
{/* Stats with counter animation */}
|
||||
<FadeOnScroll from="up" delay={0.4}>
|
||||
<div className="flex items-center justify-center gap-12 md:gap-16 mt-16 flex-wrap">
|
||||
{[
|
||||
{ stat: "40", suffix: "+", label: "Years Growing" },
|
||||
{ stat: "3", suffix: "", label: "Generations" },
|
||||
{ stat: "100", suffix: "%", label: "Hand-Picked" },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="text-center">
|
||||
<div className="text-4xl md:text-5xl lg:text-6xl font-black text-white" style={{ textShadow: "0 4px 30px rgba(16,185,129,0.3)" }}>
|
||||
<span className="counter-animate" data-target={parseInt(item.stat, 10)}>0</span>
|
||||
<span style={{ color: "#10b981" }}>{item.suffix}</span>
|
||||
</div>
|
||||
<div className="mt-2 text-[10px] uppercase tracking-[0.2em] text-stone-500">
|
||||
{item.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FadeOnScroll>
|
||||
|
||||
<FadeOnScroll from="up" delay={0.5}>
|
||||
<div className="mt-16">
|
||||
<Link
|
||||
href="/tuxedo/about"
|
||||
className="group inline-flex items-center gap-3 rounded-full px-10 py-4 text-sm font-bold transition-all duration-300"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
||||
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(-3px)";
|
||||
e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = "translateY(0)";
|
||||
e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||
}}
|
||||
>
|
||||
<span>Read Our Story</span>
|
||||
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</FadeOnScroll>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</section>
|
||||
<StorySection />
|
||||
</main>
|
||||
|
||||
<StorefrontFooter
|
||||
brandName={brand?.name ?? "Tuxedo Corn"}
|
||||
brandName={state.brand?.name ?? "Tuxedo Corn"}
|
||||
brandSlug="tuxedo"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
customFooterText={customFooterText}
|
||||
contactEmail={contactEmail}
|
||||
contactPhone={contactPhone}
|
||||
isAdmin={isAdmin}
|
||||
logoUrl={state.logoUrl}
|
||||
logoUrlDark={state.logoUrlDark}
|
||||
customFooterText={state.customFooterText}
|
||||
contactEmail={state.contactEmail}
|
||||
contactPhone={state.contactPhone}
|
||||
isAdmin={state.isAdmin}
|
||||
brandAccent="green"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user