da09cfbd1a
Deploy to route.crispygoat.com / deploy (push) Successful in 4m15s
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
1117 lines
45 KiB
TypeScript
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’t travel.
|
|
<br className="hidden sm:block" />
|
|
<span className="text-amber-200/95"> It shouldn’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’t airbrush the field
|
|
because we don’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’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]">
|
|
“You can’t replicate the diurnal swing in a
|
|
greenhouse. You can’t replicate it on a flat field.
|
|
You can’t replicate it in a hurry.”
|
|
</p>
|
|
<p className="mt-5 text-center text-[10px] uppercase tracking-[0.32em] text-stone-500">
|
|
— 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'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 — 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'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>
|
|
);
|
|
}
|