Files
route-commerce/src/app/tuxedo/page.tsx
T
Nora da09cfbd1a
Deploy to route.crispygoat.com / deploy (push) Successful in 4m15s
feat(tuxedo): editorial photo color grade across hero/pull-quote/founder/packshot
Pulls the saturated-yellow WP-import JPGs down a half-step in the brightness hierarchy so the white type stays dominant and the photos read as editorial, not product photography.

- globals.css: new filter utilities (.filter-editorial, .filter-editorial-shell with cool/amber split-tone, .filter-editorial-strong, .filter-editorial-vignette) + reduced-motion reset
- TuxedoVideoHero: grade the Ken Burns backdrop image
- tuxedo/page.tsx: apply grades to PullQuoteBand, TheCorn ear macro, FounderStrip (also swap to the retouched founder image from b90f0fa), and HarvestEditorial packshot; bump founder quality 88 -> 92
2026-07-06 14:23:22 -06:00

1117 lines
45 KiB
TypeScript

"use client";
import { useReducer, useEffect, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { m as motion, useInView } from "framer-motion";
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
import CinematicShowcase from "@/components/storefront/CinematicShowcase";
import LayoutContainer from "@/components/layout/LayoutContainer";
import ProductCard from "@/components/storefront/ProductCard";
import PaginatedStops from "@/components/storefront/PaginatedStops";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations";
import { supabase } from "@/lib/supabase";
function scrollToStops() {
document.getElementById("stops")?.scrollIntoView({ behavior: "smooth" });
}
function scrollToStory() {
document.getElementById("story")?.scrollIntoView({ behavior: "smooth" });
}
function scrollToProducts() {
document.getElementById("products")?.scrollIntoView({ behavior: "smooth" });
}
import { getBrandSettingsPublic } from "@/actions/brand-settings";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// Register GSAP plugins
if (typeof window !== "undefined") {
gsap.registerPlugin(ScrollTrigger);
}
type Brand = {
id: string;
name: string;
slug: string;
};
type Stop = {
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
slug: string;
brand_id: string;
};
type Product = {
id: string;
name: string;
description: string | null;
price: number;
type: string;
image_url: string | null;
brand_id: string;
is_taxable?: boolean;
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;
heroImageUrl: string | null;
heroVideoUrl: 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;
heroImageUrl: string | null;
heroVideoUrl: 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,
heroImageUrl: null,
heroVideoUrl: null,
isAdmin: false,
customFooterText: null,
contactEmail: null,
contactPhone: null,
brandPrimaryColor: null,
brandBgColor: null,
brandTextColor: null,
};
/**
* Editorial imagery for the Tuxedo storefront, sourced from the original
* tuxedocorn.com WordPress library and re-hosted on our MinIO bucket so
* the brand controls its own media. These are stable, brand-specific
* assets — they don't need to be admin-configurable yet. (If we ever
* add more brands, lift these into `brand_settings` columns.)
*/
const WP_IMAGES = {
/** Wide panoramic corn row with irrigation canal + Colorado sky — the
* hero poster. Replaces the earlier tight corn-cob macro. */
heroField: "https://s3.crispygoat.com/videos/wp-import/images/tuxedo-field-irrigation.jpg",
/** Pre-storm purple sky over deep-green tasseled rows receding to
* mountains — the editorial pull-quote band. */
stormField: "https://s3.crispygoat.com/videos/wp-import/images/tuxedo-field-storm.jpg",
/** White-haired founder inspecting tassels — the Our Story anchor.
* Editorial-graded version of the original WP shot — golden-hour
* lighting, shallow DOF, sharper focus on the face. Same man,
* same overalls, same tassels — just the photo he deserves. */
founderPortrait: "https://s3.crispygoat.com/videos/wp-import/images/tuxedo-founder-john-editorial.jpg",
/** Single yellow ear with silk and dark green bokeh — the product
* moment in the editorial "Why" replacement. */
earMacro: "https://s3.crispygoat.com/videos/wp-import/images/tuxedo-ear-macro.jpg",
/** Packing-shed packshot — dozens of husked cobs piled on the table
* the morning of pick. Documentary, not styled; reads like a
* magazine harvest spread rather than a styled product photo. Used
* as the full-bleed anchor of the "Harvest, by the numbers" section. */
piledCobs: "https://s3.crispygoat.com/videos/wp-import/images/corn-field-zane-original.jpg",
} as const;
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,
heroImageUrl: action.heroImageUrl,
heroVideoUrl: action.heroVideoUrl,
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 };
}
}
// ── Pull-quote band — full-bleed editorial interlude ────────────────
// A single Fraunces italic quote floating over the pre-storm field.
// No chrome. The image does the talking.
function PullQuoteBand() {
return (
<section
className="relative isolate overflow-hidden"
style={{ minHeight: "min(86vh, 820px)" }}
>
<Image
src={WP_IMAGES.stormField}
alt="Pre-storm corn rows receding to the mountains at dusk"
fill
sizes="100vw"
quality={88}
priority
className="object-cover -z-10 filter-editorial"
/>
{/* Layered tints: top darkens the sky, bottom grounds the type. */}
<div className="absolute inset-0 bg-gradient-to-b from-stone-950/55 via-stone-950/15 to-stone-950/80 -z-10" />
{/* One warm amber rim along the lower edge to echo the corn. */}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-px bg-amber-300/40 -z-10"
/>
<div className="relative min-h-[inherit] flex items-end pb-20 sm:pb-28">
<div className="mx-auto w-full max-w-5xl px-6 sm:px-10 text-center">
<FadeOnScroll from="up">
<p
className="font-display text-white text-[clamp(1.85rem,4.4vw,3.75rem)] leading-[1.08] tracking-[-0.015em] italic"
style={{ textWrap: "balance" }}
>
The best sweet corn doesn&rsquo;t travel.
<br className="hidden sm:block" />
<span className="text-amber-200/95"> It shouldn&rsquo;t have to.</span>
</p>
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.15}>
<div className="mt-10 mx-auto h-px w-12 bg-amber-300/70" />
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.25}>
<p className="mt-6 text-[10px] sm:text-[11px] font-medium uppercase tracking-[0.32em] text-stone-300">
Tuxedo Corn · Olathe, Colorado
</p>
</FadeOnScroll>
</div>
</div>
</section>
);
}
// ── The Corn — single editorial moment replacing the feature grid ───
// One paragraph. One image. The feature-card grid read as a templated
// list of icons; an honest photograph of a single ear communicates
// everything those eight tiles couldn't.
function TheCorn() {
return (
<section
id="the-corn"
className="relative bg-stone-50 py-24 sm:py-32 overflow-hidden"
>
<LayoutContainer>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-center">
{/* Left: copy column, 5/12 */}
<div className="lg:col-span-5 order-2 lg:order-1">
<FadeOnScroll from="left">
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
The Corn
</p>
</FadeOnScroll>
<FadeOnScroll from="left" delay={0.1}>
<h2
className="font-display text-stone-950 text-[clamp(2.25rem,4.8vw,3.75rem)] leading-[1.04] tracking-[-0.018em]"
style={{ textWrap: "balance" }}
>
You can taste<br className="hidden sm:block" />
<span className="italic text-amber-700/90">the altitude.</span>
</h2>
</FadeOnScroll>
<FadeOnScroll from="left" delay={0.2}>
<div className="mt-7 h-px w-12 bg-emerald-700/60" />
</FadeOnScroll>
<FadeOnScroll from="left" delay={0.3}>
<p className="mt-7 text-stone-700 text-lg leading-[1.65]">
Olathe Sweet was developed for the Uncompahgre Valley five thousand,
three hundred and sixty feet above the sea, where the daytime sun hits
the husk and the nights cool it back down before sunrise. That diurnal
swing is what loads every kernel with sugar; nothing we do in the
kitchen can replicate it, and nothing in a greenhouse can grow it.
</p>
</FadeOnScroll>
<FadeOnScroll from="left" delay={0.4}>
<p className="mt-5 text-stone-600 leading-[1.65]">
The ear in the photograph was picked the morning it was shot. The
silk on the right is still damp. We don&rsquo;t airbrush the field
because we don&rsquo;t need to.
</p>
</FadeOnScroll>
</div>
{/* Right: macro photo, 7/12 — the photograph carries the section. */}
<div className="lg:col-span-7 order-1 lg:order-2">
<FadeOnScroll from="right" duration={1}>
<div className="relative aspect-[5/4] sm:aspect-[16/11] overflow-hidden rounded-2xl shadow-[0_30px_80px_-30px_rgba(15,40,20,0.45)] ring-1 ring-stone-900/5 filter-editorial-shell">
<Image
src={WP_IMAGES.earMacro}
alt="A single ear of Olathe Sweet corn, partially husked, silk still attached, against a field of dark green tassels"
fill
sizes="(max-width: 1024px) 100vw, 58vw"
quality={90}
priority
className="object-cover"
/>
{/* Subtle amber rim light along the bottom — picks up the corn. */}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-1/3 bg-gradient-to-t from-amber-200/15 to-transparent pointer-events-none"
/>
{/* Provenance plate, lower-left, hairline border. */}
<div className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 px-3.5 py-2 rounded-full bg-stone-950/65 backdrop-blur-sm text-[10px] font-semibold uppercase tracking-[0.28em] text-amber-100">
Olathe Sweet · Day of pick
</div>
</div>
</FadeOnScroll>
</div>
</div>
</LayoutContainer>
</section>
);
}
// ── Founder strip — Our Story anchor ─────────────────────────────────
// Image + narrative. The previous "Our Story" was a centered text block;
// here the founder's portrait grounds the timeline that follows.
function FounderStrip() {
return (
<section
className="relative bg-stone-950 py-20 sm:py-24 border-y border-white/[0.04] overflow-hidden"
>
{/* A single amber rim along the top so this section reads as the
hinge between The Corn (warm cream) and the dark Story panel. */}
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 h-px bg-amber-300/30"
/>
<LayoutContainer>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14 items-center">
<FadeOnScroll from="left" className="lg:col-span-5">
<div className="relative aspect-[4/5] sm:aspect-[5/6] overflow-hidden rounded-2xl ring-1 ring-white/10 shadow-[0_30px_80px_-30px_rgba(0,0,0,0.7)] filter-editorial-shell">
<Image
src={WP_IMAGES.founderPortrait}
alt="John Harold, founder, walking through his corn rows inspecting the tassels"
fill
sizes="(max-width: 1024px) 100vw, 42vw"
quality={92}
priority
className="object-cover"
/>
{/* Soft amber wash on the bottom — matches The Corn plate. */}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-amber-200/15 to-transparent pointer-events-none"
/>
<div className="absolute bottom-4 left-4 sm:bottom-5 sm:left-5 px-3 py-1.5 rounded-full bg-stone-950/70 backdrop-blur-sm text-[10px] font-semibold uppercase tracking-[0.28em] text-amber-100">
John Harold · Olathe, CO
</div>
</div>
</FadeOnScroll>
<div className="lg:col-span-7">
<FadeOnScroll from="right">
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/80 mb-5">
The family behind the row
</p>
</FadeOnScroll>
<FadeOnScroll from="right" delay={0.1}>
<h3
className="font-display text-white text-[clamp(1.85rem,3.6vw,2.85rem)] leading-[1.1] tracking-[-0.012em]"
style={{ textWrap: "balance" }}
>
John Harold&rsquo;s great-grandchildren still walk
the same rows he cleared in 1982.
</h3>
</FadeOnScroll>
<FadeOnScroll from="right" delay={0.2}>
<p className="mt-7 text-stone-300 leading-[1.7] text-lg max-w-xl">
The Olathe Sweet parent line was rescued from a retiring farmer
who believed his valley could grow sugar in a husk. We brought
it back to the same soil, learned how it liked the water, and
kept the seed by hand, ear by ear for forty summers. The
name on the cooler is the name on the deed.
</p>
</FadeOnScroll>
<FadeOnScroll from="right" delay={0.3}>
<div className="mt-9 flex flex-wrap items-center gap-x-8 gap-y-3 text-[11px] uppercase tracking-[0.28em]">
<span className="text-amber-200/80">Est. 1982</span>
<span className="text-stone-700">·</span>
<span className="text-stone-400">3 generations on the row</span>
<span className="text-stone-700">·</span>
<span className="text-stone-400">~60 hands at peak</span>
</div>
</FadeOnScroll>
</div>
</div>
</LayoutContainer>
</section>
);
}
// ── Harvest editorial — magazine "by the numbers" spread ─────────
//
// One packshot + one data strip. The strip is the brand voice — it
// treats the farm's actual numbers (years, generations, altitude, hands)
// as magazine sidebar content, not as a feature-card grid. Hairline
// rules, large tabular numerals, and a single editorial caption beneath.
// No icons, no boxes, no color highlights. Reads as Cereal magazine
// did a feature on this farm, not as a farm-stand template.
function HarvestEditorial() {
return (
<section
className="relative bg-stone-950 py-24 sm:py-32 overflow-hidden"
aria-labelledby="harvest-heading"
>
{/* Hairline top rule — single amber line so the section reads as
a magazine spread opener. */}
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 h-px bg-amber-300/15"
/>
<LayoutContainer>
{/* Section header */}
<header className="mb-14 sm:mb-20 grid grid-cols-1 lg:grid-cols-12 gap-x-10 gap-y-6 items-end">
<div className="lg:col-span-7">
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-5">
Harvest · 2026
</p>
<h2
id="harvest-heading"
className="font-display text-stone-100 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
style={{ textWrap: "balance" }}
>
The shape of an<br className="hidden sm:block" />
<span className="italic text-amber-200/85">Olathe summer.</span>
</h2>
</div>
<div className="lg:col-span-5">
<p className="text-stone-400 text-base leading-[1.7] lg:max-w-md lg:ml-auto">
Forty summers of the same seed, the same soil, and the same
row. The diurnal swing that loads the kernel with sugar is
a property of this valley not a recipe we can take with us.
</p>
</div>
</header>
{/* Full-bleed packshot */}
<figure className="relative aspect-[16/9] sm:aspect-[21/10] overflow-hidden ring-1 ring-white/[0.06] shadow-[0_40px_120px_-40px_rgba(0,0,0,0.7)] filter-editorial-vignette">
<Image
src={WP_IMAGES.piledCobs}
alt="Husked Olathe Sweet cobs piled on the packing shed table the morning of pick"
fill
sizes="(max-width: 1024px) 100vw, 1280px"
quality={92}
priority
className="object-cover animate-harvest-drift filter-editorial-strong"
/>
{/* A single warm tint along the bottom to echo the existing
amber-wash treatment used in The Corn and FounderStrip so
the three sections feel like one editorial system. */}
<div
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t from-amber-200/10 to-transparent pointer-events-none"
/>
{/* Provenance plate, lower-left, hairline-bordered, matching
the editorial chrome used elsewhere on the page. */}
<figcaption className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 px-3.5 py-2 rounded-full bg-stone-950/65 backdrop-blur-sm text-[10px] font-semibold uppercase tracking-[0.28em] text-amber-100">
Harvest morning · August · Olathe, CO
</figcaption>
{/* Subtle photo credit on the right — like a magazine spread */}
<div className="absolute bottom-4 right-4 sm:bottom-6 sm:right-6 hidden sm:block text-[10px] uppercase tracking-[0.28em] text-amber-100/65 font-mono">
Frame 014 · zane
</div>
</figure>
{/* Magazine data strip — four columns, hairline-ruled, tabular
numerals count up on scroll-in (uses the existing .counter-animate
GSAP setup). Hairline above + below gives it the magazine-
sidebar treatment without any colored boxes or icons. */}
<ol className="mt-16 sm:mt-20 grid grid-cols-2 lg:grid-cols-4 gap-y-12 lg:gap-y-0 lg:gap-x-10 border-t border-b border-amber-300/15">
<DataPoint
value={40}
suffix="+"
label="Years growing"
detail="Olathe Sweet since 1982"
/>
<DataPoint
value={3}
label="Generations"
detail="The Harold family"
/>
<DataPoint
value={5360}
label="Feet above sea"
detail="Uncompahgre Valley"
/>
<DataPoint
value={60}
prefix="~"
label="Hands at peak"
detail="Summer harvest"
/>
</ol>
{/* Editor's caption — italic display serif, the editorial
footnote that makes the section feel like a feature story. */}
<p className="mt-16 sm:mt-20 max-w-2xl mx-auto text-center font-display italic text-stone-300 text-[clamp(1.1rem,1.8vw,1.4rem)] leading-[1.45]">
&ldquo;You can&rsquo;t replicate the diurnal swing in a
greenhouse. You can&rsquo;t replicate it on a flat field.
You can&rsquo;t replicate it in a hurry.&rdquo;
</p>
<p className="mt-5 text-center text-[10px] uppercase tracking-[0.32em] text-stone-500">
&mdash; John Harold, second generation
</p>
</LayoutContainer>
</section>
);
}
// One column of the "by the numbers" strip. The number is a
// <span className="counter-animate" data-target="N">0</span> so the
// existing GSAP setup in this page's useEffect picks it up and counts
// up from 0 to N when the column scrolls into view. Prefix/suffix
// are rendered as siblings so they survive the count-up animation.
function DataPoint({
value,
prefix,
suffix,
label,
detail,
}: {
value: number;
prefix?: string;
suffix?: string;
label: string;
detail: string;
}) {
return (
<li className="relative pt-8 lg:pt-12">
{/* Top hairline per column so the strip reads as four magazine
sidebar cells, not four feature cards. */}
<span
aria-hidden="true"
className="absolute inset-x-0 top-0 h-px bg-amber-300/20 lg:hidden"
/>
<p className="font-display text-stone-100 text-[clamp(2.75rem,5.6vw,4.25rem)] leading-none tracking-[-0.03em] tabular-nums">
{prefix ? (
<span className="text-stone-500 mr-0.5">{prefix}</span>
) : null}
<span className="counter-animate" data-target={value}>
0
</span>
{suffix ? (
<span className="text-amber-300 ml-0.5">{suffix}</span>
) : null}
</p>
<p className="mt-4 text-[10px] font-bold uppercase tracking-[0.3em] text-amber-300/85">
{label}
</p>
<p className="mt-2 text-stone-400 text-sm leading-snug max-w-[22ch]">
{detail}
</p>
</li>
);
}
// ── Wholesale link banner ───────────────────────────────────────────
function WholesaleBar({ visible }: { visible: boolean }) {
if (!visible) return null;
return (
<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&#39;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&apos;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 — slim timeline (founder portrait now lives above) ─
function StorySection() {
return (
<section id="story" className="py-24 sm:py-28 bg-stone-950 relative overflow-hidden">
{/* Single soft golden-hour glow behind the headline. */}
<div
aria-hidden="true"
className="absolute inset-0 bg-[radial-gradient(ellipse_at_50%_55%,rgba(232,163,23,0.10)_0%,transparent_55%)]"
/>
<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-[0.3em] text-amber-300/80 mb-4">
Our Story
</p>
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.1}>
<h2
className="font-display text-white text-[clamp(2.25rem,4.6vw,3.75rem)] leading-[1.04] tracking-[-0.018em] mb-8"
style={{ textWrap: "balance" }}
>
Three generations of<br className="hidden md:block" /> sweet corn excellence.
</h2>
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.2}>
<div className="mx-auto mt-6 mb-10 h-px w-16 bg-gradient-to-r from-emerald-500 to-amber-400" />
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.3}>
<p className="text-stone-300 leading-[1.7] 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&apos;s high-altitude mountain climate and grown by the same family for over 40 years.
</p>
</FadeOnScroll>
{/* Timeline — kept as the trust signal. */}
<FadeOnScroll from="up" delay={0.4}>
<ol className="mt-16 grid grid-cols-1 sm:grid-cols-3 gap-10 sm:gap-6 max-w-3xl mx-auto text-left">
{[
{
year: "1982",
head: "A seed saved",
body: "The original Olathe Sweet parent line was kept by a farmer who believed his valley could grow sugar in a husk.",
},
{
year: "2003",
head: "A field retired, then revived",
body: "After a five-year pause the family brought the same seed back to the same soil — and the sweetness came back with it.",
},
{
year: "Today",
head: "Three generations on the row",
body: "Grandparents, parents, and kids all work the summer pick — a workforce of about sixty hands during peak season.",
},
].map((item) => (
<li key={item.year} className="relative pl-6">
<span className="absolute left-0 top-1 bottom-1 w-px bg-gradient-to-b from-emerald-500/60 to-amber-400/40" />
<p className="font-display text-[11px] font-bold uppercase tracking-[0.25em] text-emerald-300/80 mb-2 tabular-nums">
{item.year}
</p>
<h3 className="text-white font-bold text-base tracking-tight mb-2">
{item.head}
</h3>
<p className="text-stone-400 text-sm leading-relaxed">
{item.body}
</p>
</li>
))}
</ol>
</FadeOnScroll>
<FadeOnScroll from="up" delay={0.5}>
<div className="mt-14">
<Link
href="/tuxedo/about"
className="group inline-flex items-center gap-3 rounded-full bg-gradient-to-br from-emerald-700 to-emerald-600 px-10 py-4 text-sm font-bold text-white shadow-[0_8px_32px_rgba(16,185,129,0.3),inset_0_1px_0_rgba(255,255,255,0.2)] hover:-translate-y-0.5 hover:shadow-[0_12px_40px_rgba(16,185,129,0.4),inset_0_1px_0_rgba(255,255,255,0.2)] transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-300 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-950"
>
<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 [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);
useEffect(() => {
async function load() {
const slug = "tuxedo";
const [brandResult, settingsResult] = await Promise.all([
supabase.from("brands").select("*").eq("slug", slug).single(),
getBrandSettingsPublic(slug),
]);
const brandData = brandResult.data as Brand | null;
dispatch({ type: "SET_BRAND", brand: brandData });
if (settingsResult.success && settingsResult.settings) {
const s = settingsResult.settings;
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,
heroImageUrl: s.hero_image_url ?? null,
heroVideoUrl: s.hero_video_url ?? 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();
dispatch({ type: "SET_IS_ADMIN", isAdmin: !!adminUser });
} catch {
// not logged in as admin
}
if (brandData?.id) {
const [{ data: stopsData }, { data: productsData }] = await Promise.all([
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 },
]);
dispatch({ type: "SET_STOPS", stops: stopsData ?? [] });
dispatch({ type: "SET_PRODUCTS", products: productsData ?? [] });
}
}
load();
}, []);
// ─── GSAP SCROLL ANIMATIONS ──────────────────────────────────────────────
// Now explicitly scoped + resilient guards (no more "invalid scope" warnings)
useEffect(() => {
if (typeof window === "undefined") return;
const ctx = gsap.context(() => {
// Parallax floating elements
gsap.utils.toArray<Element>(".parallax-float").forEach((el, i) => {
gsap.to(el, {
y: -60 - i * 15,
scrollTrigger: {
trigger: el,
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});
});
// Staggered text reveal on scroll
const revealTextElements = gsap.utils.toArray<Element>(".reveal-text");
revealTextElements.forEach((el) => {
gsap.fromTo(
el,
{ opacity: 0, y: 40, skewY: 2 },
{
opacity: 1,
y: 0,
skewY: 0,
duration: 1,
ease: "power3.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none reverse",
},
}
);
});
// Scale reveal for cards
const revealScaleElements = gsap.utils.toArray<Element>(".reveal-scale");
revealScaleElements.forEach((el, i) => {
gsap.fromTo(
el,
{ opacity: 0, scale: 0.9, y: 40 },
{
opacity: 1,
scale: 1,
y: 0,
duration: 0.8,
delay: i * 0.08,
ease: "back.out(1.4)",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none reverse",
},
}
);
});
// Counter animations (uses reliable proxy like before; now also has reduced-motion guard for resilience)
const prefersReduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
const counterElements = gsap.utils.toArray<Element>(".counter-animate");
counterElements.forEach((el) => {
const target = parseInt(el.getAttribute("data-target") || "0", 10);
if (prefersReduced) {
el.textContent = target.toLocaleString();
return;
}
const obj = { val: 0 };
gsap.to(obj, {
val: target,
duration: 2,
ease: "power2.out",
onUpdate: () => {
if (el.textContent !== null) {
el.textContent = Math.floor(obj.val).toLocaleString();
}
},
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none none",
},
});
});
// Horizontal reveal for section headers
gsap.utils.toArray<Element>(".reveal-slide").forEach((el) => {
gsap.fromTo(
el,
{ opacity: 0, x: -40 },
{
opacity: 1,
x: 0,
duration: 0.8,
ease: "power3.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none reverse",
},
}
);
});
}, pageScopeRef);
return () => ctx.revert();
}, []);
return (
<div ref={pageScopeRef} className="min-h-screen bg-stone-50">
<BrandStylesProvider
primaryColor={state.brandPrimaryColor}
bgColor={state.brandBgColor}
textColor={state.brandTextColor}
/>
<StorefrontHeader
brandName={state.brand?.name ?? "Tuxedo Corn"}
brandSlug="tuxedo"
logoUrl={state.logoUrl}
logoUrlDark={state.logoUrlDark}
showWholesaleLink={state.showWholesaleLink}
isAdmin={state.isAdmin}
brandAccent="green"
/>
<main>
<WholesaleBar visible={state.showWholesaleLink && state.wholesaleEnabled} />
<TuxedoVideoHero
eyebrow="Olathe Sweet™ — Olathe, Colorado"
title="Tuxedo Corn"
description={
state.heroTagline ??
"Premium Olathe Sweet™ sweet corn — hand-picked at peak freshness from our family farm in Colorado."
}
olatheSweetLogoUrl={state.olatheSweetLogoUrl}
videoUrl={state.heroVideoUrl}
posterUrl={state.heroImageUrl}
primaryButton="Find a Stop"
secondaryButton="Our Story"
onPrimaryClick={scrollToStops}
onSecondaryClick={scrollToStory}
/>
<PullQuoteBand />
<TheCorn />
<StopsSection stops={state.stops} showSchedulePdf={state.showSchedulePdf} />
<SectionDivider />
<ProductsSection
products={state.products}
brandName={state.brand?.name ?? "Tuxedo Corn"}
olatheSweetLogoUrlDark={state.olatheSweetLogoUrlDark}
/>
<FounderStrip />
<HarvestEditorial />
<StorySection />
</main>
<StorefrontFooter
brandName={state.brand?.name ?? "Tuxedo Corn"}
brandSlug="tuxedo"
logoUrl={state.logoUrl}
logoUrlDark={state.logoUrlDark}
customFooterText={state.customFooterText}
contactEmail={state.contactEmail}
contactPhone={state.contactPhone}
isAdmin={state.isAdmin}
brandAccent="green"
/>
</div>
);
}