"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 (
Pre-storm corn rows receding to the mountains at dusk {/* Layered tints: top darkens the sky, bottom grounds the type. */}
{/* One warm amber rim along the lower edge to echo the corn. */}
); } // ── 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 (
{/* Left: copy column, 5/12 */}

The Corn

You can taste
the altitude.

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.

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.

{/* Right: macro photo, 7/12 — the photograph carries the section. */}
A single ear of Olathe Sweet corn, partially husked, silk still attached, against a field of dark green tassels {/* Subtle amber rim light along the bottom — picks up the corn. */}
); } // ── 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 (
{/* A single amber rim along the top so this section reads as the hinge between The Corn (warm cream) and the dark Story panel. */}
); } // ── 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 (
{/* Hairline top rule — single amber line so the section reads as a magazine spread opener. */}
); } // One column of the "by the numbers" strip. The number is a // 0 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 (
  • {/* Top hairline per column so the strip reads as four magazine sidebar cells, not four feature cards. */}
  • ); } // ── Wholesale link banner ─────────────────────────────────────────── function WholesaleBar({ visible }: { visible: boolean }) { if (!visible) return null; return (
    Wholesale Portal →
    ); } // ── Stops section ─────────────────────────────────────────────────── function StopsSection({ stops, showSchedulePdf }: { stops: Stop[]; showSchedulePdf: boolean }) { return (
    {/* Parallax background layers */}

    Delivery Stops

    Upcoming
    Stops

    Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.

    {showSchedulePdf && ( Download Schedule )}
    {stops.length === 0 ? (

    No stops on the calendar just yet

    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.

    View All Stops {showSchedulePdf && ( Download Schedule )}
    ) : ( )}
    ); } // ── Section divider ───────────────────────────────────────────────── function SectionDivider() { return (
    ); } // ── Products section ──────────────────────────────────────────────── function ProductsSection({ products, brandName, olatheSweetLogoUrlDark, }: { products: Product[]; brandName: string; olatheSweetLogoUrlDark: string | null; }) { return (
    {/* Scroll-driven reveal for section header */}

    Farm-Direct

    This Week's
    Harvest

    Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.

    {/* Cinematic showcase replaces static grid */} ({ 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 */}
    {products.slice(0, 6).map((product) => ( ))}
    {products.length > 3 && (

    {products.length - 3} more products available in the full catalog.

    )}
    ); } // ── Story section — slim timeline (founder portrait now lives above) ─ function StorySection() { return (
    {/* Single soft golden-hour glow behind the headline. */}
    ); } 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(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(".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(".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(".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(".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(".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 (
    ); }