tuxedo: align storefront subpages with Field Almanac editorial voice
Deploy to route.crispygoat.com / deploy (push) Successful in 4m25s
Deploy to route.crispygoat.com / deploy (push) Successful in 4m25s
Reskin /tuxedo/contact, /tuxedo/faq, /tuxedo/stops, and /tuxedo/stops/[id] so they match the homepage's editorial magazine language (Fraunces display serif, stone-50/stone-950 alternation, amber rims, hairline rules, tabular numerals). - About page rewritten as inlined editorial sections sharing one WP_IMAGES library with the homepage; the four lazy-loaded Tuxedo about components are removed (Indian River has its own variants). - Contact page now uses a dark hero with heroField half-bleed, a hairline-ruled info strip (address/phone/email), a two-column form section with bottom-border editorial inputs, and a closing Fraunces italic quote. /tuxedo/contact was 404ing in prod because page.tsx was missing — created and removed the duplicated render from layout.tsx. - FAQ page uses a cream hero with Fraunces headline, an editorial bottom-border search, hairline-ruled accordion rows (no card wrappers), and a dark closing CTA with Contact pill. - Stops list uses a dark hero with heroField bleed and a cream hairline-ruled sidebar list: tabular date numerals on the left, city/location middle, mono time + cutoff right, chevron for upcoming rows. - Stop detail uses a dark hero pairing the city/state headline with a giant tabular day stamp, a cream hairline-ruled 4-col info strip (Date/Time/Location/Order by), and the editorial products header. Removed the dual-brand isBlue branching now that Indian River has its own page. - Extracted TUXEDO_IMAGES to src/components/storefront/tuxedo-images.ts so the homepage, about, contact, and stops list share one brand-specific asset library. - /tuxedo/faq/layout.tsx no longer double-renders the page component (was rendering <FAQClientPage /> alongside children). Verified: tsc --noEmit clean, eslint clean (only pre-existing homepage warnings), npm run build produces all Tuxedo routes including the now-existing /tuxedo/contact.
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Suspense, lazy, useState } from "react";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { m as motion } from "framer-motion";
|
import Link from "next/link";
|
||||||
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
|
import { TUXEDO_IMAGES as WP_IMAGES } from "@/components/storefront/tuxedo-images";
|
||||||
const MissionSection = lazy(() => import("@/components/about/MissionSection"));
|
import { FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
||||||
const FamilyTimelineSection = lazy(() => import("@/components/about/FamilyTimelineSection"));
|
|
||||||
const ContactSection = lazy(() => import("@/components/about/ContactSection"));
|
|
||||||
const CTASection = lazy(() => import("@/components/about/CTASection"));
|
|
||||||
|
|
||||||
interface AboutClientProps {
|
interface AboutClientProps {
|
||||||
logoUrl: string | null;
|
logoUrl: string | null;
|
||||||
@@ -28,7 +25,6 @@ interface AboutClientProps {
|
|||||||
export default function AboutClient({
|
export default function AboutClient({
|
||||||
logoUrl,
|
logoUrl,
|
||||||
logoUrlDark,
|
logoUrlDark,
|
||||||
olatheSweetLogoUrlDark,
|
|
||||||
aboutHeadline,
|
aboutHeadline,
|
||||||
aboutSubheadline,
|
aboutSubheadline,
|
||||||
showWholesaleLink,
|
showWholesaleLink,
|
||||||
@@ -51,101 +47,25 @@ export default function AboutClient({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{/* Hero Section */}
|
<HeroSection
|
||||||
<section className="relative bg-stone-950 overflow-hidden">
|
headline={aboutHeadline ?? "Three generations of sweet corn excellence."}
|
||||||
{/* Decorative elements */}
|
subheadline={
|
||||||
<div className="absolute inset-0 opacity-20">
|
aboutSubheadline ??
|
||||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-emerald-900/20 rounded-full blur-3xl" />
|
"Forty summers of the same seed, the same soil, and the same row — grown at five thousand, three hundred and sixty feet, picked by hand, packed the morning of."
|
||||||
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-amber-900/10 rounded-full blur-3xl" />
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Pattern overlay */}
|
<TheCornSection />
|
||||||
<div className="absolute inset-0 opacity-5" style={{ backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(255,255,255,0.1) 35px, rgba(255,255,255,0.1) 70px)' }} />
|
|
||||||
|
|
||||||
<div className="relative mx-auto max-w-6xl px-6 py-24 md:py-36">
|
<PullQuoteBand />
|
||||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
|
||||||
{/* Text content */}
|
|
||||||
<div>
|
|
||||||
<motion.p
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6 }}
|
|
||||||
className="text-[11px] font-bold uppercase tracking-[0.3em] text-emerald-400/70 mb-4"
|
|
||||||
>
|
|
||||||
Our Heritage
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.h1
|
<FounderStrip />
|
||||||
initial={{ opacity: 0, y: 30 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.15 }}
|
|
||||||
className="text-5xl md:text-6xl lg:text-7xl font-black text-white tracking-tight leading-[1] mb-6"
|
|
||||||
>
|
|
||||||
{aboutHeadline ?? "Three Generations of Sweet Corn Excellence"}
|
|
||||||
</motion.h1>
|
|
||||||
|
|
||||||
<motion.p
|
<FamilyTimeline />
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.6, delay: 0.3 }}
|
|
||||||
className="text-xl md:text-2xl text-stone-300 leading-relaxed"
|
|
||||||
>
|
|
||||||
{aboutSubheadline ?? "Over 40 years of growing and shipping Olathe Sweet Sweet Corn from our family to yours."}
|
|
||||||
</motion.p>
|
|
||||||
|
|
||||||
<motion.div
|
<VisitTheFarm address={address} />
|
||||||
initial={{ scaleX: 0 }}
|
|
||||||
animate={{ scaleX: 1 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.5 }}
|
|
||||||
className="mt-8 h-1 w-16 bg-gradient-to-r from-emerald-500 to-amber-500 rounded-full origin-left"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Logo */}
|
<CTASection />
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.2 }}
|
|
||||||
className="flex justify-center lg:justify-end"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute -inset-6 bg-emerald-900/30 rounded-full blur-2xl" />
|
|
||||||
<Image
|
|
||||||
src={olatheSweetLogoUrlDark ?? "/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png"}
|
|
||||||
alt="Olathe Sweet"
|
|
||||||
width={288}
|
|
||||||
height={288}
|
|
||||||
unoptimized
|
|
||||||
className="relative w-56 md:w-72 h-auto object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom transition */}
|
|
||||||
<div className="absolute -bottom-px left-0 right-0 h-3 bg-stone-50" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Mission Section */}
|
|
||||||
<Suspense fallback={<LoadingFallback bg="bg-stone-50" />}>
|
|
||||||
<MissionSection />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{/* Family Timeline */}
|
|
||||||
<Suspense fallback={<LoadingFallback bg="bg-stone-100" />}>
|
|
||||||
<FamilyTimelineSection />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{/* Contact Section */}
|
|
||||||
<Suspense fallback={<LoadingFallback bg="bg-stone-950" />}>
|
|
||||||
<ContactSection address={address} />
|
|
||||||
</Suspense>
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<Suspense fallback={<LoadingFallback bg="bg-white" />}>
|
|
||||||
<CTASection />
|
|
||||||
</Suspense>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StorefrontFooter
|
<StorefrontFooter
|
||||||
@@ -163,6 +83,537 @@ export default function AboutClient({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoadingFallback({ bg }: { bg: string }) {
|
// ── HERO — dark editorial opener ────────────────────────────────────
|
||||||
return <div className={`py-24 md:py-32 ${bg}`} />;
|
// Same architectural language as the homepage FounderStrip, but with
|
||||||
|
// the portrait as a full anchor instead of a supporting figure, and
|
||||||
|
// the headline sitting in the same Fraunces display register as the
|
||||||
|
// homepage "Three generations of sweet corn excellence" copy.
|
||||||
|
function HeroSection({
|
||||||
|
headline,
|
||||||
|
subheadline,
|
||||||
|
}: {
|
||||||
|
headline: string;
|
||||||
|
subheadline: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-950 overflow-hidden">
|
||||||
|
{/* A single amber rim along the bottom — same hinge used
|
||||||
|
throughout the homepage between cream and dark sections. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 bottom-0 h-px bg-amber-300/30 z-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="relative py-20 sm:py-28 lg:py-32 grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-center">
|
||||||
|
{/* Copy column — 7/12 on desktop */}
|
||||||
|
<div className="lg:col-span-7 order-2 lg:order-1">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-6">
|
||||||
|
Our Heritage · Est. 1982
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
|
<h1
|
||||||
|
className="font-display text-stone-50 text-[clamp(2.5rem,5.4vw,4.5rem)] leading-[1.02] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
{headline.split(/(excellence\.)/i).map((part, i) =>
|
||||||
|
/excellence\.$/i.test(part) ? (
|
||||||
|
<span key={i} className="italic text-amber-200/90">
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span key={i}>{part}</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.18}>
|
||||||
|
<div className="mt-8 h-px w-16 bg-gradient-to-r from-emerald-500 via-amber-400 to-amber-300/0" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.26}>
|
||||||
|
<p className="mt-8 text-stone-300 text-lg sm:text-xl leading-[1.65] max-w-xl">
|
||||||
|
{subheadline}
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.34}>
|
||||||
|
<div className="mt-10 flex flex-wrap items-center gap-x-8 gap-y-3 text-[11px] uppercase tracking-[0.28em]">
|
||||||
|
<span className="text-amber-200/80">Olathe, Colorado</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
|
<span className="text-stone-400">5,360 ft</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
|
<span className="text-stone-400">Uncompahgre Valley</span>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Portrait column — 5/12 on desktop */}
|
||||||
|
<div className="lg:col-span-5 order-1 lg:order-2">
|
||||||
|
<FadeOnScroll from="right" duration={0.4}>
|
||||||
|
<div className="relative aspect-[4/5] sm:aspect-[5/6] overflow-hidden rounded-2xl ring-1 ring-white/10 shadow-[0_40px_120px_-40px_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 homepage
|
||||||
|
FounderStrip treatment. */}
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── THE CORN — split editorial moment ───────────────────────────────
|
||||||
|
// Mirrors the homepage's "The Corn" section: copy left (5/12),
|
||||||
|
// single macro photograph right (7/12). One paragraph. One image.
|
||||||
|
// The feature-card grid read as a templated list of icons; a single
|
||||||
|
// honest photograph communicates everything those tiles couldn't.
|
||||||
|
function TheCornSection() {
|
||||||
|
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 */}
|
||||||
|
<div className="lg:col-span-7 order-1 lg:order-2">
|
||||||
|
<FadeOnScroll from="right" duration={0.4}>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PULL-QUOTE BAND — full-bleed editorial interlude ───────────────
|
||||||
|
// Same composition the homepage uses: pre-storm field behind, single
|
||||||
|
// Fraunces italic quote floating in the lower third. No chrome. The
|
||||||
|
// image does the talking.
|
||||||
|
function PullQuoteBand() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative isolate overflow-hidden"
|
||||||
|
style={{ minHeight: "min(70vh, 640px)" }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={WP_IMAGES.stormField}
|
||||||
|
alt="Pre-storm corn rows receding to the mountains at dusk"
|
||||||
|
fill
|
||||||
|
sizes="100vw"
|
||||||
|
quality={88}
|
||||||
|
className="object-cover -z-10 filter-editorial"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-stone-950/55 via-stone-950/15 to-stone-950/80 -z-10" />
|
||||||
|
<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-16 sm:pb-24">
|
||||||
|
<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.75rem,3.8vw,3.25rem)] leading-[1.08] tracking-[-0.015em] italic"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
“The field decides the sweetness.<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="text-amber-200/95">We just pick it on time.”</span>
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.15}>
|
||||||
|
<div className="mt-8 mx-auto h-px w-12 bg-amber-300/70" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.25}>
|
||||||
|
<p className="mt-5 text-[10px] sm:text-[11px] font-medium uppercase tracking-[0.32em] text-stone-300">
|
||||||
|
— Tuxedo Corn · Olathe, Colorado
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FOUNDER STRIP — narrative anchor for "The People" section ──────
|
||||||
|
// Cream background, magazine spread. Eyebrow + Fraunces headline on
|
||||||
|
// the left, two paragraphs of founder story on the right. Below it
|
||||||
|
// the FamilyTimeline component renders the four family-member entries
|
||||||
|
// in the same hairline-ruled editorial register.
|
||||||
|
function FounderStrip() {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-50 py-24 sm:py-32 overflow-hidden border-t border-stone-200/60">
|
||||||
|
<LayoutContainer>
|
||||||
|
<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">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
|
The People Behind the Corn
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.1}>
|
||||||
|
<h2
|
||||||
|
className="font-display text-stone-950 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
The Harold family,
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-700/90">on the same row.</span>
|
||||||
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<FadeOnScroll from="up" delay={0.15}>
|
||||||
|
<p className="text-stone-600 text-base leading-[1.7] lg:max-w-md lg:ml-auto">
|
||||||
|
John Harold began selling Olathe’s sweet corn directly
|
||||||
|
from pickups in the 1970s. The Olathe Sweet parent line was
|
||||||
|
rescued from a retiring farmer who believed his valley could
|
||||||
|
grow sugar in a husk. Three generations later, the name on
|
||||||
|
the cooler is still the name on the deed.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FAMILY TIMELINE — magazine "sidebar" register ──────────────────
|
||||||
|
// Hairline-ruled four-column grid. No cards, no avatars in gradient
|
||||||
|
// boxes. The homepage's HarvestEditorial data strip is the structural
|
||||||
|
// model; the era + name + role + bio columns are the editorial content.
|
||||||
|
// Order matters: founder first, then by generation.
|
||||||
|
const FAMILY = [
|
||||||
|
{
|
||||||
|
initials: "JH",
|
||||||
|
name: "John Harold",
|
||||||
|
role: "Founder",
|
||||||
|
era: "1970s",
|
||||||
|
bio: "Started selling Olathe's sweet corn directly from pickups. Built Tuxedo Corn into a family operation rooted in the Uncompahgre Valley.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initials: "DH",
|
||||||
|
name: "David Harold",
|
||||||
|
role: "Second Generation",
|
||||||
|
era: "Present",
|
||||||
|
bio: "Leads sustainability across Harold farms — drip irrigation, Fair Food Program, reduced chemical inputs. Keeps the soil honest.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initials: "JW",
|
||||||
|
name: "John William Harold",
|
||||||
|
role: "Mexico Operations",
|
||||||
|
era: "1990s–Present",
|
||||||
|
bio: "Three decades in Guaymas, Sonora, growing sweet corn, onions, wheat, and vegetables. Extends the Harold tradition across borders.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initials: "JH",
|
||||||
|
name: "Joseph Harold",
|
||||||
|
role: "Third Generation",
|
||||||
|
era: "Present",
|
||||||
|
bio: "Works the corn shed in summer, handles photography and beverages. Carrying the family tradition into the next generation.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function FamilyTimeline() {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-50 pb-24 sm:pb-32 overflow-hidden">
|
||||||
|
<LayoutContainer>
|
||||||
|
{/* Hairline above the strip — the magazine sidebar opening. */}
|
||||||
|
<div aria-hidden="true" className="h-px w-full bg-stone-900/15 mb-14" />
|
||||||
|
|
||||||
|
<ol className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-y-14 lg:gap-y-0 lg:gap-x-10">
|
||||||
|
{FAMILY.map((member, i) => (
|
||||||
|
<FadeOnScroll from="up" delay={i * 0.08} key={member.name}>
|
||||||
|
<li className="relative lg:pt-2">
|
||||||
|
{/* Per-column hairline (top on desktop, absent on mobile
|
||||||
|
because the gap-y supplies spacing). */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 -top-6 h-px bg-stone-900/15 hidden lg:block"
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-[0.32em] text-emerald-700 tabular-nums">
|
||||||
|
{member.era}
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 font-display text-stone-950 text-[clamp(1.65rem,2.4vw,2.1rem)] leading-[1.1] tracking-[-0.014em]">
|
||||||
|
{member.name}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[11px] font-semibold uppercase tracking-[0.22em] text-amber-700/85">
|
||||||
|
{member.role}
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-stone-600 text-sm leading-[1.6] max-w-[28ch]">
|
||||||
|
{member.bio}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
</FadeOnScroll>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{/* Hairline + caption underneath, matching the HarvestEditorial
|
||||||
|
editorial-footer rhythm. */}
|
||||||
|
<div aria-hidden="true" className="h-px w-full bg-stone-900/15 mt-16" />
|
||||||
|
<p className="mt-8 text-center text-[10px] uppercase tracking-[0.32em] text-stone-500">
|
||||||
|
Three generations · One valley · Forty summers
|
||||||
|
</p>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── VISIT THE FARM — dark editorial address block ──────────────────
|
||||||
|
// Replaces the three icon-card contact layout. Stone-950 panel with a
|
||||||
|
// magazine "by the numbers" data strip + a single hairline-ruled
|
||||||
|
// address line. No SVG icons in colored boxes; the typography carries
|
||||||
|
// the section.
|
||||||
|
function VisitTheFarm({ address }: { address: string }) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative bg-stone-950 py-24 sm:py-32 overflow-hidden"
|
||||||
|
aria-labelledby="visit-heading"
|
||||||
|
>
|
||||||
|
{/* Single amber rim at the top — same hinge used on the dark
|
||||||
|
homepage sections. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 top-0 h-px bg-amber-300/15"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LayoutContainer>
|
||||||
|
<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">
|
||||||
|
The Farm
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
id="visit-heading"
|
||||||
|
className="font-display text-stone-100 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
Visit the<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-200/85">Uncompahgre Valley.</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">
|
||||||
|
The farm is open during harvest. Pickup windows are tight,
|
||||||
|
shed hours are short, and the corn is best the morning it’s
|
||||||
|
picked — call ahead before you drive out.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Hairline-ruled data strip — four columns: address / phone /
|
||||||
|
shipping hours / office hours. Same magazine-sidebar treatment
|
||||||
|
as the homepage HarvestEditorial numbers strip, just with
|
||||||
|
prose instead of counters. */}
|
||||||
|
<ol className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-y-14 lg:gap-y-0 lg:gap-x-10 border-t border-b border-amber-300/15">
|
||||||
|
<FarmPoint label="Address" detail={address} mono />
|
||||||
|
<FarmPoint label="Shipping" detail="970-323-5631" mono />
|
||||||
|
<FarmPoint label="Office" detail="970-323-6874" mono />
|
||||||
|
<FarmPoint
|
||||||
|
label="Hours"
|
||||||
|
detail={["Mon–Fri · 7A–6P", "Sat–Sun · 8A–2P"]}
|
||||||
|
/>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{/* Editorial caption, italic display serif. */}
|
||||||
|
<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]">
|
||||||
|
“We don’t ship corn that’s more than a day from
|
||||||
|
the field. The valley won’t let us.”
|
||||||
|
</p>
|
||||||
|
<p className="mt-5 text-center text-[10px] uppercase tracking-[0.32em] text-stone-500">
|
||||||
|
— Tuxedo Corn · Olathe, Colorado
|
||||||
|
</p>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One column of the Visit-the-Farm strip. Same hairline rules and
|
||||||
|
// label style as HarvestEditorial's DataPoint on the homepage, with
|
||||||
|
// `detail` either a string or an array of lines (for the Hours column).
|
||||||
|
function FarmPoint({
|
||||||
|
label,
|
||||||
|
detail,
|
||||||
|
mono = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
detail: string | string[];
|
||||||
|
mono?: boolean;
|
||||||
|
}) {
|
||||||
|
const lines = Array.isArray(detail) ? detail : [detail];
|
||||||
|
return (
|
||||||
|
<li className="relative pt-8 lg:pt-12">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-[0.3em] text-amber-300/85">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<p
|
||||||
|
key={i}
|
||||||
|
className={`mt-4 text-stone-100 text-base leading-[1.55] ${
|
||||||
|
mono ? "font-mono tabular-nums tracking-tight" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CTA — closing editorial pair ───────────────────────────────────
|
||||||
|
// Stone-50 section with the dark gradient emerald→amber pill button
|
||||||
|
// used on the homepage StorySection "Read Our Story" CTA, plus a
|
||||||
|
// secondary "Shop Products" outline. Same voice as the homepage's
|
||||||
|
// closing beat: "Find a stop / Shop products".
|
||||||
|
function CTASection() {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-50 py-24 sm:py-32 overflow-hidden border-t border-stone-200/60">
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="relative mx-auto max-w-3xl text-center">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
|
This Week
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
|
<h2
|
||||||
|
className="font-display text-stone-950 text-[clamp(2rem,4vw,3rem)] leading-[1.05] tracking-[-0.018em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
Find a stop near you,
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-700/90">or have it shipped.</span>
|
||||||
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.16}>
|
||||||
|
<div className="mx-auto mt-7 h-px w-16 bg-gradient-to-r from-emerald-500 to-amber-400" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.24}>
|
||||||
|
<p className="mt-7 text-stone-600 text-lg leading-[1.65]">
|
||||||
|
Preorder for pickup at a stop on the summer route, or have
|
||||||
|
cooler boxes shipped directly to your door after the season.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.32}>
|
||||||
|
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
|
||||||
|
<Link
|
||||||
|
href="/tuxedo#stops"
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-gradient-to-br from-emerald-700 to-emerald-600 px-9 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-50"
|
||||||
|
>
|
||||||
|
<span>Find a Stop</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}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/tuxedo#products"
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-white px-9 py-4 text-sm font-bold text-stone-900 ring-1 ring-stone-200 hover:ring-emerald-600 hover:text-emerald-700 transition-all"
|
||||||
|
>
|
||||||
|
<span>Shop Products</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}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import { TUXEDO_IMAGES } from "@/components/storefront/tuxedo-images";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
||||||
|
|
||||||
type BrandSettings = {
|
type BrandSettings = {
|
||||||
invoice_business_name: string | null;
|
invoice_business_name: string | null;
|
||||||
@@ -22,6 +24,11 @@ type FormState = {
|
|||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FALLBACK_ADDRESS = "59751 David Road, Olathe, CO 81425";
|
||||||
|
const FALLBACK_EMAIL = "orders@tuxedocorn.com";
|
||||||
|
const SHIPPING_PHONE = "970-323-5631";
|
||||||
|
const OFFICE_PHONE = "970-323-6874";
|
||||||
|
|
||||||
export default function TuxedoContactPage() {
|
export default function TuxedoContactPage() {
|
||||||
const [brandSettings, setBrandSettings] = useState<BrandSettings | null>(null);
|
const [brandSettings, setBrandSettings] = useState<BrandSettings | null>(null);
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
@@ -30,12 +37,16 @@ export default function TuxedoContactPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
|
||||||
supabase
|
import("@/lib/supabase").then(({ supabase }) => {
|
||||||
.from("wholesale_settings")
|
supabase
|
||||||
.select("invoice_business_name, invoice_business_address, invoice_business_phone, invoice_business_email, invoice_business_website")
|
.from("wholesale_settings")
|
||||||
.eq("brand_id", TUXEDO_BRAND_ID)
|
.select(
|
||||||
.single()
|
"invoice_business_name, invoice_business_address, invoice_business_phone, invoice_business_email, invoice_business_website"
|
||||||
.then(({ data }) => setBrandSettings((data as unknown as BrandSettings) ?? null));
|
)
|
||||||
|
.eq("brand_id", TUXEDO_BRAND_ID)
|
||||||
|
.single()
|
||||||
|
.then(({ data }) => setBrandSettings((data as unknown as BrandSettings) ?? null));
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent) {
|
function handleSubmit(e: React.FormEvent) {
|
||||||
@@ -47,182 +58,531 @@ export default function TuxedoContactPage() {
|
|||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const address = brandSettings?.invoice_business_address ?? FALLBACK_ADDRESS;
|
||||||
|
const email = brandSettings?.invoice_business_email ?? FALLBACK_EMAIL;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
||||||
|
|
||||||
<main className="py-16 md:py-20">
|
<main>
|
||||||
<LayoutContainer>
|
<ContactHero />
|
||||||
<div className="max-w-5xl mx-auto">
|
|
||||||
|
|
||||||
{/* Page header */}
|
{/* Contact info strip — hairline-ruled four-column data grid,
|
||||||
<div className="text-center mb-16">
|
same magazine-sidebar register as the About page's
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Get in Touch</p>
|
"Visit the Farm" section. */}
|
||||||
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">
|
<ContactInfoStrip
|
||||||
Contact Us
|
address={address}
|
||||||
</h1>
|
email={email}
|
||||||
<p className="mt-5 text-lg text-stone-500 max-w-xl mx-auto leading-relaxed">
|
shippingPhone={SHIPPING_PHONE}
|
||||||
Questions about corn, stops, or wholesale accounts — we are happy to help.
|
officePhone={OFFICE_PHONE}
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact info grid */}
|
{/* Form section — cream stone-50, two-column layout. Left
|
||||||
<div className="grid gap-6 md:grid-cols-3 mb-16">
|
column carries the editorial framing (eyebrow + Fraunces
|
||||||
<div className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center">
|
headline + paragraph + small hours caption). Right column
|
||||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50 mb-5">
|
holds the form itself, no wrapper white card. */}
|
||||||
<svg className="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
<section className="relative bg-stone-50 py-24 sm:py-32 border-t border-stone-200/60">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
<LayoutContainer>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16">
|
||||||
</svg>
|
{/* Left: editorial framing */}
|
||||||
</div>
|
<div className="lg:col-span-5">
|
||||||
<h3 className="text-base font-bold text-stone-950 mb-3">Farm Address</h3>
|
<FadeOnScroll from="up">
|
||||||
<p className="text-sm text-stone-500 leading-relaxed">
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
{brandSettings?.invoice_business_address ?? "59751 David Road, Olathe, CO 81425"}
|
Send a Message
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</FadeOnScroll>
|
||||||
<div className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center">
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50 mb-5">
|
<h2
|
||||||
<svg className="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
className="font-display text-stone-950 text-[clamp(2rem,4.2vw,3.25rem)] leading-[1.05] tracking-[-0.02em]"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.184-.046-.379-.041-.545.114L18 10.48a2.25 2.25 0 00-.545-.114l-4.423 1.106c-.5.119-.852.575-.852 1.091v1.372a2.25 2.25 0 01-2.25 2.25h-2.25a15 15 0 01-15-15z" />
|
style={{ textWrap: "balance" }}
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-bold text-stone-950 mb-3">Phone</h3>
|
|
||||||
<a href="tel:9703235631" className="block text-sm text-stone-500 hover:text-emerald-700 transition-colors leading-relaxed">
|
|
||||||
Shipping: 970-323-5631
|
|
||||||
</a>
|
|
||||||
<a href="tel:9703236874" className="mt-1 block text-sm text-stone-500 hover:text-emerald-700 transition-colors leading-relaxed">
|
|
||||||
Office: 970-323-6874
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center">
|
|
||||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-emerald-50 mb-5">
|
|
||||||
<svg className="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-base font-bold text-stone-950 mb-3">Email</h3>
|
|
||||||
<a href={`mailto:${brandSettings?.invoice_business_email ?? "orders@tuxedocorn.com"}`} className="text-sm text-stone-500 hover:text-emerald-700 transition-colors">
|
|
||||||
{brandSettings?.invoice_business_email ?? "orders@tuxedocorn.com"}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Contact form */}
|
|
||||||
<div className="rounded-3xl bg-white p-10 shadow-sm ring-1 ring-stone-200/60">
|
|
||||||
<div className="mb-8">
|
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-3">Send a Message</p>
|
|
||||||
<h2 className="text-3xl font-black text-stone-950 tracking-tight leading-tight">
|
|
||||||
Get in Touch
|
|
||||||
</h2>
|
|
||||||
<div className="mt-4 h-px w-12 bg-emerald-600" />
|
|
||||||
</div>
|
|
||||||
{submitted ? (
|
|
||||||
<div className="rounded-2xl bg-emerald-50 p-10 text-center ring-1 ring-emerald-100">
|
|
||||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 mb-4">
|
|
||||||
<svg className="h-6 w-6 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-xl font-bold text-stone-950">Message received.</p>
|
|
||||||
<p className="mt-2 text-sm text-stone-500">We will be in touch within 1-2 business days.</p>
|
|
||||||
<button type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSubmitted(false);
|
|
||||||
setForm({ name: "", email: "", topic: "general", message: "" });
|
|
||||||
}}
|
|
||||||
className="mt-5 text-sm font-medium text-emerald-700 underline hover:text-emerald-900"
|
|
||||||
>
|
>
|
||||||
Send another message
|
Tell us what you’re after,
|
||||||
</button>
|
<br className="hidden sm:block" />{" "}
|
||||||
</div>
|
<span className="italic text-amber-700/90">we’ll write back.</span>
|
||||||
) : (
|
</h2>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
</FadeOnScroll>
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<FadeOnScroll from="up" delay={0.16}>
|
||||||
|
<div className="mt-7 h-px w-12 bg-emerald-700/60" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.24}>
|
||||||
|
<p className="mt-7 text-stone-700 text-lg leading-[1.65]">
|
||||||
|
Most messages get a reply within one business day. During
|
||||||
|
harvest weeks the office is open early — call if your
|
||||||
|
order is time-sensitive and we’ll get you a real
|
||||||
|
answer, not a form letter.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
{/* Hours caption, hairline-ruled inline. */}
|
||||||
|
<FadeOnScroll from="up" delay={0.32}>
|
||||||
|
<dl className="mt-12 border-t border-stone-900/15 pt-6 grid grid-cols-2 gap-y-5 gap-x-6 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="fld-1-your-name" className="block text-sm font-semibold text-stone-700 mb-2">Your Name</label>
|
<dt className="text-[10px] font-bold uppercase tracking-[0.3em] text-emerald-700">
|
||||||
<input id="fld-1-your-name" aria-label="Jane Smith"
|
Weekdays
|
||||||
required
|
</dt>
|
||||||
value={form.name}
|
<dd className="mt-2 font-mono tabular-nums text-stone-950">
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
7A – 6P
|
||||||
className="w-full rounded-xl border border-stone-200 bg-white px-5 py-4 text-sm text-stone-900 placeholder-stone-400 outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900 transition-colors"
|
</dd>
|
||||||
placeholder="Jane Smith"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="fld-2-email" className="block text-sm font-semibold text-stone-700 mb-2">Email</label>
|
<dt className="text-[10px] font-bold uppercase tracking-[0.3em] text-emerald-700">
|
||||||
<input id="fld-2-email" aria-label="Jane@example.com"
|
Weekends
|
||||||
required
|
</dt>
|
||||||
type="email"
|
<dd className="mt-2 font-mono tabular-nums text-stone-950">
|
||||||
value={form.email}
|
8A – 2P
|
||||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
</dd>
|
||||||
className="w-full rounded-xl border border-stone-200 bg-white px-5 py-4 text-sm text-stone-900 placeholder-stone-400 outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900 transition-colors"
|
|
||||||
placeholder="jane@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</dl>
|
||||||
<div>
|
</FadeOnScroll>
|
||||||
<label htmlFor="fld-3-topic" className="block text-sm font-semibold text-stone-700 mb-2">Topic</label>
|
</div>
|
||||||
<select id="fld-3-topic" aria-label="Select"
|
|
||||||
value={form.topic}
|
{/* Right: form */}
|
||||||
onChange={(e) => setForm({ ...form, topic: e.target.value })}
|
<div className="lg:col-span-7">
|
||||||
className="w-full rounded-xl border border-stone-200 bg-white px-5 py-4 text-sm text-stone-900 outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900 transition-colors appearance-none"
|
<FadeOnScroll from="right">
|
||||||
>
|
{submitted ? (
|
||||||
<option value="general">General Question</option>
|
<FormSuccess
|
||||||
<option value="orders">Orders & Pickup</option>
|
onReset={() => {
|
||||||
<option value="wholesale">Wholesale Inquiry</option>
|
setSubmitted(false);
|
||||||
<option value="media">Media & Partnership</option>
|
setForm({ name: "", email: "", topic: "general", message: "" });
|
||||||
</select>
|
}}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="fld-4-message" className="block text-sm font-semibold text-stone-700 mb-2">Message</label>
|
|
||||||
<textarea id="fld-4-message" aria-label="How Can We Help You?"
|
|
||||||
required
|
|
||||||
rows={5}
|
|
||||||
value={form.message}
|
|
||||||
onChange={(e) => setForm({ ...form, message: e.target.value })}
|
|
||||||
className="w-full rounded-xl border border-stone-200 bg-white px-5 py-4 text-sm text-stone-900 placeholder-stone-400 outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900 transition-colors resize-none"
|
|
||||||
placeholder="How can we help you?"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<button
|
<ContactForm
|
||||||
type="submit"
|
form={form}
|
||||||
disabled={isSubmitting}
|
setForm={setForm}
|
||||||
className="rounded-xl bg-stone-900 px-8 py-4 text-sm font-bold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:shadow-lg hover:shadow-black/15"
|
onSubmit={handleSubmit}
|
||||||
>
|
isSubmitting={isSubmitting}
|
||||||
{isSubmitting ? "Sending..." : "Send Message"}
|
/>
|
||||||
</button>
|
)}
|
||||||
</form>
|
</FadeOnScroll>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Business hours — elevated */}
|
<ContactClosingQuote />
|
||||||
<div className="mt-10 rounded-3xl bg-white p-10 shadow-sm ring-1 ring-stone-200/60">
|
|
||||||
<div className="mb-6">
|
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-3">Summer Season</p>
|
|
||||||
<h2 className="text-2xl font-black text-stone-950 tracking-tight leading-tight">
|
|
||||||
When to Reach Us
|
|
||||||
</h2>
|
|
||||||
<div className="mt-4 h-px w-12 bg-emerald-600" />
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-stone-50">
|
|
||||||
<p className="font-semibold text-stone-700">Monday – Friday</p>
|
|
||||||
<p className="text-stone-500 font-medium">7:00 AM – 6:00 PM</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-stone-50">
|
|
||||||
<p className="font-semibold text-stone-700">Saturday – Sunday</p>
|
|
||||||
<p className="text-stone-500 font-medium">8:00 AM – 2:00 PM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-5 text-sm text-stone-400 leading-relaxed">
|
|
||||||
During the off-season, response times may be longer. For urgent matters, please call the office.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LayoutContainer>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HERO — dark editorial opener ────────────────────────────────────
|
||||||
|
// Mirrors the About page's hero structure: eyebrow + Fraunces headline
|
||||||
|
// (with italic emphasis) + subhead + hairline gradient rule. The
|
||||||
|
// field photograph sits as a soft half-bleed at the bottom-right
|
||||||
|
// instead of a featured portrait — the contact page is shorter and
|
||||||
|
// quieter than the about page.
|
||||||
|
function ContactHero() {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-950 overflow-hidden isolate">
|
||||||
|
{/* Soft field image, anchored bottom-right, partially visible
|
||||||
|
behind the headline column. Suggests the farm without
|
||||||
|
competing with the form. */}
|
||||||
|
<div className="absolute inset-y-0 right-0 w-full lg:w-1/2 -z-10">
|
||||||
|
<Image
|
||||||
|
src={TUXEDO_IMAGES.heroField}
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
|
quality={85}
|
||||||
|
priority
|
||||||
|
className="object-cover object-center filter-editorial opacity-50"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-stone-950 via-stone-950/85 to-stone-950/30" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-stone-950/40 via-transparent to-stone-950" />
|
||||||
|
</div>
|
||||||
|
{/* Single amber rim along the bottom — same hinge as the
|
||||||
|
homepage and about dark sections. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 bottom-0 h-px bg-amber-300/30 z-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="relative py-20 sm:py-28 lg:py-32 max-w-3xl">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-6">
|
||||||
|
Get in Touch · Olathe, Colorado
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
|
<h1
|
||||||
|
className="font-display text-stone-50 text-[clamp(2.5rem,5.4vw,4.5rem)] leading-[1.02] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
Send a note,
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-200/90">or a question.</span>
|
||||||
|
</h1>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.18}>
|
||||||
|
<div className="mt-8 h-px w-16 bg-gradient-to-r from-emerald-500 via-amber-400 to-amber-300/0" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.26}>
|
||||||
|
<p className="mt-8 text-stone-300 text-lg sm:text-xl leading-[1.65] max-w-xl">
|
||||||
|
Orders, pickup logistics, wholesale accounts, press — every
|
||||||
|
message lands on a real desk in Olathe and gets a real
|
||||||
|
answer.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
|
||||||
|
<FadeOnScroll from="up" delay={0.34}>
|
||||||
|
<div className="mt-10 flex flex-wrap items-center gap-x-8 gap-y-3 text-[11px] uppercase tracking-[0.28em]">
|
||||||
|
<span className="text-amber-200/80">Replies in 1–2 days</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
|
<span className="text-stone-400">No form letters</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
|
<span className="text-stone-400">Real people</span>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CONTACT INFO STRIP — magazine "by the numbers" pattern ─────────
|
||||||
|
// Same hairline-ruled four-column data grid as the About page's
|
||||||
|
// "Visit the Farm" section. Address, shipping phone, office phone,
|
||||||
|
// email — no icon-in-rounded-box, just type and tabular numerals.
|
||||||
|
function ContactInfoStrip({
|
||||||
|
address,
|
||||||
|
email,
|
||||||
|
shippingPhone,
|
||||||
|
officePhone,
|
||||||
|
}: {
|
||||||
|
address: string;
|
||||||
|
email: string;
|
||||||
|
shippingPhone: string;
|
||||||
|
officePhone: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-50 py-20 sm:py-24">
|
||||||
|
<LayoutContainer>
|
||||||
|
<ol className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-y-12 lg:gap-y-0 lg:gap-x-10 border-t border-b border-stone-900/15">
|
||||||
|
<InfoPoint label="Address" detail={address} />
|
||||||
|
<InfoPoint label="Shipping" detail={shippingPhone} mono />
|
||||||
|
<InfoPoint label="Office" detail={officePhone} mono />
|
||||||
|
<InfoPoint label="Email" detail={email} mono />
|
||||||
|
</ol>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoPoint({
|
||||||
|
label,
|
||||||
|
detail,
|
||||||
|
mono = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
detail: string;
|
||||||
|
mono?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="relative pt-8 lg:pt-12">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-[0.3em] text-emerald-700">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`mt-4 text-stone-950 text-base leading-[1.55] ${
|
||||||
|
mono ? "font-mono tabular-nums tracking-tight" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{detail}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FORM — bottom-border-only editorial inputs ─────────────────────
|
||||||
|
// The form sits directly on the cream background without a wrapping
|
||||||
|
// card. Inputs use bottom borders only (with a hairline focus state),
|
||||||
|
// matching the "atelier-input" treatment used elsewhere in the admin.
|
||||||
|
// The submit button is the same emerald→amber gradient pill used on
|
||||||
|
// the homepage and about pages.
|
||||||
|
function ContactForm({
|
||||||
|
form,
|
||||||
|
setForm,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
}: {
|
||||||
|
form: FormState;
|
||||||
|
setForm: (s: FormState) => void;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<form onSubmit={onSubmit} className="space-y-10">
|
||||||
|
<div className="grid gap-10 sm:grid-cols-2">
|
||||||
|
<Field
|
||||||
|
id="fld-name"
|
||||||
|
label="Your Name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(v) => setForm({ ...form, name: v })}
|
||||||
|
required
|
||||||
|
placeholder="Jane Smith"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
id="fld-email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={form.email}
|
||||||
|
onChange={(v) => setForm({ ...form, email: v })}
|
||||||
|
required
|
||||||
|
placeholder="jane@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
id="fld-topic"
|
||||||
|
label="Topic"
|
||||||
|
value={form.topic}
|
||||||
|
onChange={(v) => setForm({ ...form, topic: v })}
|
||||||
|
options={[
|
||||||
|
{ value: "general", label: "General Question" },
|
||||||
|
{ value: "orders", label: "Orders & Pickup" },
|
||||||
|
{ value: "wholesale", label: "Wholesale Inquiry" },
|
||||||
|
{ value: "media", label: "Media & Partnership" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextareaField
|
||||||
|
id="fld-message"
|
||||||
|
label="Message"
|
||||||
|
value={form.message}
|
||||||
|
onChange={(v) => setForm({ ...form, message: v })}
|
||||||
|
required
|
||||||
|
placeholder="How can we help?"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-gradient-to-br from-emerald-700 to-emerald-600 px-9 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-50 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:translate-y-0"
|
||||||
|
>
|
||||||
|
<span>{isSubmitting ? "Sending…" : "Send Message"}</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}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single text input — bottom-border-only editorial treatment.
|
||||||
|
// Label sits above the input as a small uppercase eyebrow so the
|
||||||
|
// field is readable on the cream background without a chrome card.
|
||||||
|
function Field({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
required,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-[10px] font-bold uppercase tracking-[0.28em] text-stone-500 mb-3"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
required={required}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full bg-transparent border-0 border-b border-stone-900/15 px-0 py-3 text-base text-stone-950 placeholder-stone-400 outline-none focus:border-emerald-700 focus:border-b-2 focus:pb-[11px] transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectField({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-[10px] font-bold uppercase tracking-[0.28em] text-stone-500 mb-3"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full appearance-none bg-transparent border-0 border-b border-stone-900/15 px-0 py-3 pr-8 text-base text-stone-950 outline-none focus:border-emerald-700 focus:border-b-2 focus:pb-[11px] transition-colors"
|
||||||
|
>
|
||||||
|
{options.map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none absolute right-0 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TextareaField({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
required,
|
||||||
|
placeholder,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="block text-[10px] font-bold uppercase tracking-[0.28em] text-stone-500 mb-3"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id={id}
|
||||||
|
required={required}
|
||||||
|
rows={5}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full bg-transparent border-0 border-b border-stone-900/15 px-0 py-3 text-base text-stone-950 placeholder-stone-400 outline-none focus:border-emerald-700 focus:border-b-2 focus:pb-[11px] transition-colors resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormSuccess({ onReset }: { onReset: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="relative border-t border-b border-emerald-700/30 py-16 text-center">
|
||||||
|
<div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 mb-6">
|
||||||
|
<svg
|
||||||
|
className="h-6 w-6 text-emerald-700"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="font-display text-stone-950 text-[clamp(1.5rem,2.6vw,2.1rem)] leading-[1.1] tracking-[-0.014em]">
|
||||||
|
Message received.
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto mt-6 h-px w-12 bg-emerald-700/60" />
|
||||||
|
<p className="mt-7 text-stone-600 leading-[1.65]">
|
||||||
|
We’ll be in touch within one to two business days. If it’s
|
||||||
|
urgent, the office line is the fastest way to reach us.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onReset}
|
||||||
|
className="mt-8 text-sm font-semibold text-emerald-700 underline underline-offset-4 hover:text-emerald-900 transition-colors"
|
||||||
|
>
|
||||||
|
Send another message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLOSING — dark editorial quote ─────────────────────────────────
|
||||||
|
// Same magazine-closing beat the homepage uses: italic Fraunces quote
|
||||||
|
// centered with a hairline-rule attribution underneath.
|
||||||
|
function ContactClosingQuote() {
|
||||||
|
return (
|
||||||
|
<section className="relative bg-stone-950 py-24 sm:py-32 overflow-hidden">
|
||||||
|
{/* Top amber rim — hinges against the cream form section. */}
|
||||||
|
<div aria-hidden="true" className="absolute inset-x-0 top-0 h-px bg-amber-300/15" />
|
||||||
|
|
||||||
|
<LayoutContainer>
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p
|
||||||
|
className="mx-auto max-w-2xl text-center font-display italic text-stone-100 text-[clamp(1.4rem,2.8vw,2.1rem)] leading-[1.4] tracking-[-0.012em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
“Pick up the phone. Write us a note. Either way, you’re
|
||||||
|
reaching a person who knows the field.”
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.15}>
|
||||||
|
<div className="mx-auto mt-8 h-px w-12 bg-amber-300/70" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.25}>
|
||||||
|
<p className="mt-5 text-center text-[10px] uppercase tracking-[0.32em] text-stone-400">
|
||||||
|
— Tuxedo Corn · Olathe, Colorado
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.35}>
|
||||||
|
<div className="mt-12 text-center">
|
||||||
|
<Link
|
||||||
|
href="/tuxedo/faq"
|
||||||
|
className="text-sm font-semibold text-amber-200/85 underline underline-offset-4 hover:text-amber-100 transition-colors"
|
||||||
|
>
|
||||||
|
Read the FAQ first →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import ContactClientPage from "./ContactClientPage";
|
|
||||||
|
|
||||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
||||||
|
|
||||||
@@ -40,5 +39,5 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function TuxedoContactLayout({ children }: { children: React.ReactNode }) {
|
export default function TuxedoContactLayout({ children }: { children: React.ReactNode }) {
|
||||||
return <ContactClientPage />;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import ContactClientPage from "./ContactClientPage";
|
||||||
|
|
||||||
|
export default function TuxedoContactPage() {
|
||||||
|
return <ContactClientPage />;
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
|
import { FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
||||||
|
|
||||||
type FAQItem = {
|
type FAQItem = {
|
||||||
question: string;
|
question: string;
|
||||||
@@ -16,15 +18,18 @@ const FAQ_CATEGORIES = [
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
question: "How do I place a preorder for corn?",
|
question: "How do I place a preorder for corn?",
|
||||||
answer: "Find a stop near you on our homepage and click 'Shop This Stop' to add corn to your cart and select your pickup location. Preordering helps us bring the right amount of corn to each stop.",
|
answer:
|
||||||
|
"Find a stop near you on our homepage and click 'Shop This Stop' to add corn to your cart and select your pickup location. Preordering helps us bring the right amount of corn to each stop.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Can I order without selecting a pickup stop?",
|
question: "Can I order without selecting a pickup stop?",
|
||||||
answer: "Yes. Choose 'Shipping' at checkout and we will mail cooler boxes directly to your home. Shipping is available for orders of 4 or more dozen and is fulfilled after the season ends.",
|
answer:
|
||||||
|
"Yes. Choose 'Shipping' at checkout and we will mail cooler boxes directly to your home. Shipping is available for orders of 4 or more dozen and is fulfilled after the season ends.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "What happens if I miss my pickup time?",
|
question: "What happens if I miss my pickup time?",
|
||||||
answer: "Please call the office at 970-323-6874 as soon as possible. We will do our best to accommodate, but unclaimed orders may be donated or sold after the pickup window closes.",
|
answer:
|
||||||
|
"Please call the office at 970-323-6874 as soon as possible. We will do our best to accommodate, but unclaimed orders may be donated or sold after the pickup window closes.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -33,15 +38,18 @@ const FAQ_CATEGORIES = [
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
question: "Do you ship corn?",
|
question: "Do you ship corn?",
|
||||||
answer: "Yes. We ship Olathe Sweet Sweet Corn nationwide. Orders ship as cooler boxes after our field season ends in late summer. A minimum of 4 dozen is required for shipping.",
|
answer:
|
||||||
|
"Yes. We ship Olathe Sweet Sweet Corn nationwide. Orders ship as cooler boxes after our field season ends in late summer. A minimum of 4 dozen is required for shipping.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How are cooler boxes packaged?",
|
question: "How are cooler boxes packaged?",
|
||||||
answer: "Corn is packed in insulated coolers with gel packs to keep it fresh during transit. Shipping costs are calculated at checkout based on your location.",
|
answer:
|
||||||
|
"Corn is packed in insulated coolers with gel packs to keep it fresh during transit. Shipping costs are calculated at checkout based on your location.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "When will my shipped order arrive?",
|
question: "When will my shipped order arrive?",
|
||||||
answer: "Cooler box orders are shipped after the season ends in late summer. You will receive a tracking notification by email once your order ships. Most orders within the continental US arrive within 3-7 business days.",
|
answer:
|
||||||
|
"Cooler box orders are shipped after the season ends in late summer. You will receive a tracking notification by email once your order ships. Most orders within the continental US arrive within 3–7 business days.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -50,11 +58,13 @@ const FAQ_CATEGORIES = [
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
question: "How do I set up a wholesale account?",
|
question: "How do I set up a wholesale account?",
|
||||||
answer: "Visit /wholesale/register to apply. We review applications and respond within 1-2 business days. Wholesale accounts are available to retailers, restaurants, and farm stands.",
|
answer:
|
||||||
|
"Visit /wholesale/register to apply. We review applications and respond within 1–2 business days. Wholesale accounts are available to retailers, restaurants, and farm stands.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is there a minimum order for wholesale?",
|
question: "Is there a minimum order for wholesale?",
|
||||||
answer: "Minimum wholesale order is $100 per order. Some account types have additional minimums — contact us for details.",
|
answer:
|
||||||
|
"Minimum wholesale order is $100 per order. Some account types have additional minimums — contact us for details.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -63,21 +73,30 @@ const FAQ_CATEGORIES = [
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
question: "What makes Olathe Sweet different?",
|
question: "What makes Olathe Sweet different?",
|
||||||
answer: "Olathe Sweet Sweet Corn is grown in the Uncompahgre Valley of Colorado, where high-altitude days and cool nights produce exceptionally sweet, tender corn. We have been growing this variety for over 40 years.",
|
answer:
|
||||||
|
"Olathe Sweet Sweet Corn is grown in the Uncompahgre Valley of Colorado, where high-altitude days and cool nights produce exceptionally sweet, tender corn. We have been growing this variety for over 40 years.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is your corn organic?",
|
question: "Is your corn organic?",
|
||||||
answer: "We use regenerative farming practices including no-till methods, cover crops, and reduced chemical inputs. We are not certified organic, but we take a thoughtful approach to land stewardship.",
|
answer:
|
||||||
|
"We use regenerative farming practices including no-till methods, cover crops, and reduced chemical inputs. We are not certified organic, but we take a thoughtful approach to land stewardship.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "How should I store my corn?",
|
question: "How should I store my corn?",
|
||||||
answer: "Store corn in the refrigerator and eat within 3-5 days. Do not husk it until you are ready to cook. For best sweetness, bring a cooler with ice to transport your corn home.",
|
answer:
|
||||||
|
"Store corn in the refrigerator and eat within 3–5 days. Do not husk it until you are ready to cook. For best sweetness, bring a cooler with ice to transport your corn home.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function FAQAccordion({ items, searchQuery }: { items: FAQItem[]; searchQuery: string }) {
|
function FAQAccordion({
|
||||||
|
items,
|
||||||
|
searchQuery,
|
||||||
|
}: {
|
||||||
|
items: FAQItem[];
|
||||||
|
searchQuery: string;
|
||||||
|
}) {
|
||||||
const [open, setOpen] = useState<string | null>(null);
|
const [open, setOpen] = useState<string | null>(null);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
@@ -90,47 +109,74 @@ function FAQAccordion({ items, searchQuery }: { items: FAQItem[]; searchQuery: s
|
|||||||
);
|
);
|
||||||
}, [items, searchQuery]);
|
}, [items, searchQuery]);
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="py-10 text-center">
|
||||||
|
<p className="text-stone-500 text-sm">
|
||||||
|
No results for “{searchQuery}”
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-stone-400 text-sm">
|
||||||
|
Try a different term or browse the categories below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<ul className="divide-y divide-stone-900/15 border-t border-b border-stone-900/15">
|
||||||
{filtered.length === 0 ? (
|
{filtered.map((item) => {
|
||||||
<div className="rounded-2xl bg-white p-6 sm:p-8 text-center shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-stone-300">
|
const isOpen = open === item.question;
|
||||||
<p className="text-stone-500 text-sm">No results for "{searchQuery}"</p>
|
return (
|
||||||
<p className="mt-1 text-stone-400 text-sm">Try a different term or browse all categories below.</p>
|
<li key={item.question}>
|
||||||
</div>
|
<button
|
||||||
) : (
|
type="button"
|
||||||
<>
|
onClick={() => setOpen(isOpen ? null : item.question)}
|
||||||
{filtered.map((item) => {
|
className="group flex w-full items-start justify-between gap-6 py-6 sm:py-7 text-left"
|
||||||
const isOpen = open === item.question;
|
aria-expanded={isOpen}
|
||||||
return (
|
>
|
||||||
<div
|
<span className="font-display text-stone-950 text-[clamp(1.1rem,1.7vw,1.35rem)] leading-[1.3] tracking-[-0.01em] pr-4 group-hover:text-emerald-800 transition-colors">
|
||||||
key={item.question}
|
{item.question}
|
||||||
className="rounded-2xl bg-white shadow-sm ring-1 ring-stone-200/60 overflow-hidden transition-all duration-300 hover:ring-stone-300"
|
</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`mt-1 flex-shrink-0 inline-flex h-7 w-7 items-center justify-center transition-all duration-200 ${
|
||||||
|
isOpen
|
||||||
|
? "rotate-45 text-emerald-700"
|
||||||
|
: "text-stone-400 group-hover:text-stone-700"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<button type="button"
|
<svg
|
||||||
onClick={() => setOpen(isOpen ? null : item.question)}
|
className="w-4 h-4"
|
||||||
className="flex w-full items-center justify-between px-4 sm:px-6 py-4 sm:py-5 text-left group"
|
fill="none"
|
||||||
aria-expanded={isOpen}
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
>
|
>
|
||||||
<span className="font-semibold text-stone-950 pr-3 sm:pr-4 text-sm sm:text-[15px] leading-snug group-hover:text-emerald-700 transition-colors">
|
<path
|
||||||
{item.question}
|
strokeLinecap="round"
|
||||||
</span>
|
strokeLinejoin="round"
|
||||||
<span className={`flex-shrink-0 w-6 h-6 sm:w-7 sm:h-7 rounded-full flex items-center justify-center transition-all duration-300 ${isOpen ? "bg-emerald-600 text-white rotate-180" : "bg-stone-100 text-stone-400 group-hover:bg-stone-200"}`}>
|
d="M12 4.5v15m7.5-7.5h-15"
|
||||||
<svg className="w-3 h-3 sm:w-3.5 sm:h-3.5 transition-transform duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
/>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
</svg>
|
||||||
</svg>
|
</span>
|
||||||
</span>
|
</button>
|
||||||
</button>
|
<div
|
||||||
<div className={`overflow-hidden transition-all duration-300 ease-out ${isOpen ? "max-h-48 opacity-100" : "max-h-0 opacity-0"}`}>
|
className={`grid transition-all duration-300 ease-out ${
|
||||||
<div className="px-4 sm:px-6 pb-5 sm:pb-6 pt-1 border-t border-stone-100">
|
isOpen
|
||||||
<p className="text-sm text-stone-500 leading-relaxed">{item.answer}</p>
|
? "grid-rows-[1fr] opacity-100 pb-6 sm:pb-7"
|
||||||
</div>
|
: "grid-rows-[0fr] opacity-0"
|
||||||
</div>
|
}`}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<p className="text-stone-600 leading-[1.7] max-w-3xl pr-10">
|
||||||
|
{item.answer}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
</li>
|
||||||
</>
|
);
|
||||||
)}
|
})}
|
||||||
</div>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,63 +187,183 @@ export default function TuxedoFAQPage() {
|
|||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
||||||
|
|
||||||
<main className="py-12 sm:py-16 md:py-20 px-4 sm:px-6">
|
<main>
|
||||||
<div className="max-w-3xl mx-auto">
|
{/* Hero — cream stone-50 with editorial framing. */}
|
||||||
{/* Page header */}
|
<section className="relative bg-stone-50 pt-20 pb-12 sm:pt-24 sm:pb-16 border-b border-stone-200/60">
|
||||||
<div className="text-center mb-10 sm:mb-14">
|
<LayoutContainer>
|
||||||
<p className="text-[10px] sm:text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-3 sm:mb-4">Questions</p>
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-tight">FAQ</h1>
|
<FadeOnScroll from="up">
|
||||||
<p className="mt-4 sm:mt-5 text-base sm:text-lg text-stone-500 leading-relaxed">Everything you need to know about ordering, pickup, and shipping.</p>
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
</div>
|
Help · Frequently Asked
|
||||||
|
</p>
|
||||||
{/* Search */}
|
</FadeOnScroll>
|
||||||
<div className="mb-8 sm:mb-12 relative">
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
<div className="absolute inset-y-0 left-4 sm:left-5 flex items-center pointer-events-none">
|
<h1
|
||||||
<svg className="h-4 w-4 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
className="font-display text-stone-950 text-[clamp(2.5rem,5.2vw,4.25rem)] leading-[1.02] tracking-[-0.02em]"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
style={{ textWrap: "balance" }}
|
||||||
</svg>
|
>
|
||||||
|
Questions,
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-700/90">answered.</span>
|
||||||
|
</h1>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.18}>
|
||||||
|
<div className="mx-auto mt-8 h-px w-16 bg-gradient-to-r from-emerald-500 to-amber-400" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.26}>
|
||||||
|
<p className="mt-8 text-stone-600 text-lg sm:text-xl leading-[1.65]">
|
||||||
|
Everything you need to know about ordering, pickup, and
|
||||||
|
shipping — straight from the people who pack the boxes.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
</div>
|
</div>
|
||||||
<input aria-label="Search Questions..."
|
</LayoutContainer>
|
||||||
type="text"
|
</section>
|
||||||
placeholder="Search questions..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full rounded-2xl border border-stone-200 bg-white pl-10 sm:pl-11 pr-5 py-3.5 sm:py-4 text-sm sm:text-base text-stone-900 placeholder-stone-400 outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900/20 transition-all duration-200 shadow-sm hover:shadow-md focus:shadow-lg"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button type="button"
|
|
||||||
onClick={() => setSearchQuery("")}
|
|
||||||
className="absolute inset-y-0 right-4 flex items-center text-stone-400 hover:text-stone-600 transition-colors"
|
|
||||||
aria-label="Close">
|
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FAQ categories */}
|
{/* Search — editorial bottom-border-only input. */}
|
||||||
<div className="space-y-10 sm:space-y-14">
|
<section className="relative bg-stone-50 py-12 sm:py-16">
|
||||||
{FAQ_CATEGORIES.map((cat) => (
|
<LayoutContainer>
|
||||||
<div key={cat.category}>
|
<div className="mx-auto max-w-3xl">
|
||||||
<h2 className="text-[10px] sm:text-xs font-bold uppercase tracking-[0.2em] text-stone-400 mb-4 sm:mb-5">{cat.category}</h2>
|
<FadeOnScroll from="up">
|
||||||
<FAQAccordion items={cat.items} searchQuery={searchQuery} />
|
<div className="relative">
|
||||||
</div>
|
<label htmlFor="faq-search" className="sr-only">
|
||||||
))}
|
Search questions
|
||||||
</div>
|
</label>
|
||||||
|
<div className="absolute inset-y-0 left-0 flex items-center pointer-events-none">
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 text-stone-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="faq-search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search questions…"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full bg-transparent border-0 border-b border-stone-900/15 pl-7 pr-9 py-3 text-base text-stone-950 placeholder-stone-400 outline-none focus:border-emerald-700 focus:border-b-2 focus:pb-[11px] transition-colors"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
aria-label="Clear search"
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center text-stone-400 hover:text-stone-700 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Still have questions CTA */}
|
{/* Categories — each rendered as a category with hairline-ruled
|
||||||
<div className="mt-12 sm:mt-16 rounded-2xl sm:rounded-3xl bg-stone-950 px-6 sm:px-10 py-10 sm:py-12 text-center shadow-xl">
|
accordion list. No card wrappers; each item is a single row. */}
|
||||||
<p className="text-xl sm:text-2xl font-black text-white tracking-tight">Still have questions?</p>
|
<section className="relative bg-stone-50 pb-24 sm:pb-32">
|
||||||
<p className="mt-2 text-stone-400 text-sm">We are happy to help — reach out anytime.</p>
|
<LayoutContainer>
|
||||||
<Link href="/tuxedo/contact" className="mt-6 sm:mt-8 inline-flex items-center gap-2 rounded-full bg-emerald-600 px-6 sm:px-8 py-3 sm:py-4 font-bold text-white hover:bg-emerald-500 active:bg-emerald-700 transition-all text-sm tracking-wider hover:shadow-lg hover:shadow-emerald-900/30">
|
<div className="mx-auto max-w-3xl space-y-16 sm:space-y-20">
|
||||||
Contact Us
|
{FAQ_CATEGORIES.map((cat) => (
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<div key={cat.category}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
<FadeOnScroll from="up">
|
||||||
</svg>
|
<p className="text-[10px] font-bold uppercase tracking-[0.32em] text-emerald-700 mb-6">
|
||||||
</Link>
|
{cat.category}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</FadeOnScroll>
|
||||||
|
<FAQAccordion items={cat.items} searchQuery={searchQuery} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Closing CTA — dark editorial panel with italic Fraunces
|
||||||
|
headline + Contact button. Same closing beat the homepage
|
||||||
|
and about pages use. */}
|
||||||
|
<section className="relative bg-stone-950 py-24 sm:py-32 overflow-hidden">
|
||||||
|
{/* Top amber rim. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 top-0 h-px bg-amber-300/15"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="mx-auto max-w-2xl text-center">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-5">
|
||||||
|
Still Wondering
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
|
<h2
|
||||||
|
className="font-display text-stone-100 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
Still have
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-200/85">questions?</span>
|
||||||
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.18}>
|
||||||
|
<div className="mx-auto mt-7 h-px w-16 bg-gradient-to-r from-emerald-500 to-amber-400" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.26}>
|
||||||
|
<p className="mt-7 text-stone-400 text-lg leading-[1.65]">
|
||||||
|
We’re happy to help — reach out anytime. Most
|
||||||
|
messages get a reply within one business day.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.34}>
|
||||||
|
<div className="mt-10">
|
||||||
|
<Link
|
||||||
|
href="/tuxedo/contact"
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-gradient-to-br from-emerald-700 to-emerald-600 px-9 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>Contact Us</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}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import FAQClientPage from "./FAQClientPage";
|
|
||||||
import { serializeJsonForScript } from "@/lib/safe-json";
|
import { serializeJsonForScript } from "@/lib/safe-json";
|
||||||
|
|
||||||
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
|
||||||
@@ -144,7 +143,7 @@ export default function TuxedoFAQLayout({ children }: { children: React.ReactNod
|
|||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(organizationJsonLd) }}
|
dangerouslySetInnerHTML={{ __html: serializeJsonForScript(organizationJsonLd) }}
|
||||||
/>
|
/>
|
||||||
<FAQClientPage />
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+1
-29
@@ -12,6 +12,7 @@ import PaginatedStops from "@/components/storefront/PaginatedStops";
|
|||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
|
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
|
||||||
|
import { TUXEDO_IMAGES as WP_IMAGES } from "@/components/storefront/tuxedo-images";
|
||||||
import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
import { ScrollReveal, ParallaxLayer, FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
|
|
||||||
@@ -133,35 +134,6 @@ const initialState: State = {
|
|||||||
brandTextColor: 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 {
|
function reducer(state: State, action: Action): State {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "SET_BRAND":
|
case "SET_BRAND":
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { gsap } from "gsap";
|
import { gsap } from "gsap";
|
||||||
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import { TUXEDO_IMAGES } from "@/components/storefront/tuxedo-images";
|
||||||
import type { PublicStop } from "@/actions/stops";
|
import type { PublicStop } from "@/actions/stops";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -16,22 +18,30 @@ type Props = {
|
|||||||
|
|
||||||
export default function TuxedoStopsList({ stops, brandName, brandSlug }: Props) {
|
export default function TuxedoStopsList({ stops, brandName, brandSlug }: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [now] = useState(() => new Date());
|
||||||
|
|
||||||
|
// Subtle scroll-in stagger for the stops list. The Motion policy in
|
||||||
|
// ScrollAnimations.tsx caps translation to 16px / 320ms, which matches
|
||||||
|
// what we want here — the list reveals as a quiet beat, not a show.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
gsap.from(".stop-card", {
|
gsap.from(".stop-row", {
|
||||||
y: 30,
|
y: 16,
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
duration: 0.5,
|
duration: 0.32,
|
||||||
stagger: 0.08,
|
stagger: 0.04,
|
||||||
ease: "power3.out",
|
ease: "power1.out",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: containerRef.current,
|
||||||
|
start: "top 90%",
|
||||||
|
toggleActions: "play none none none",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, containerRef);
|
}, containerRef);
|
||||||
return () => ctx.revert();
|
return () => ctx.revert();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
const upcomingStops = stops.filter((s) => new Date(s.date) >= now);
|
const upcomingStops = stops.filter((s) => new Date(s.date) >= now);
|
||||||
const pastStops = stops.filter((s) => new Date(s.date) < now);
|
const pastStops = stops.filter((s) => new Date(s.date) < now);
|
||||||
|
|
||||||
@@ -39,123 +49,315 @@ export default function TuxedoStopsList({ stops, brandName, brandSlug }: Props)
|
|||||||
<div ref={containerRef} className="min-h-screen bg-stone-50">
|
<div ref={containerRef} className="min-h-screen bg-stone-50">
|
||||||
<StorefrontHeader brandName={brandName} brandSlug={brandSlug} brandAccent="green" />
|
<StorefrontHeader brandName={brandName} brandSlug={brandSlug} brandAccent="green" />
|
||||||
|
|
||||||
<main className="py-16 md:py-20">
|
<main>
|
||||||
<LayoutContainer>
|
{/* Hero — dark editorial opener with the field photograph as a
|
||||||
<div className="max-w-4xl mx-auto mb-12 text-center">
|
soft half-bleed at the bottom, same register as the
|
||||||
<p className="text-[11px] font-bold uppercase tracking-[0.2em] text-emerald-600 mb-4">
|
Contact page hero. */}
|
||||||
{brandName}
|
<section className="relative bg-stone-950 overflow-hidden isolate">
|
||||||
</p>
|
{/* Soft field image, bottom-anchored, partially visible
|
||||||
<h1 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 mb-4">
|
behind the headline column. */}
|
||||||
Pickup Stops
|
<div className="absolute inset-x-0 bottom-0 h-2/3 -z-10">
|
||||||
</h1>
|
<Image
|
||||||
<p className="text-stone-500 text-lg">
|
src={TUXEDO_IMAGES.heroField}
|
||||||
Fresh Olathe Sweet™ sweet corn delivered to a stop near you.
|
alt=""
|
||||||
</p>
|
fill
|
||||||
<div className="mt-6 h-1 w-12 bg-emerald-600 mx-auto rounded-full" />
|
sizes="100vw"
|
||||||
|
quality={85}
|
||||||
|
priority
|
||||||
|
className="object-cover object-center filter-editorial opacity-40"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-stone-950 via-stone-950/70 to-stone-950" />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-x-0 bottom-0 h-px bg-amber-300/30 z-10"
|
||||||
|
/>
|
||||||
|
|
||||||
{upcomingStops.length === 0 ? (
|
<LayoutContainer>
|
||||||
<div className="max-w-2xl mx-auto text-center py-16">
|
<div className="relative py-20 sm:py-28 lg:py-32 max-w-3xl">
|
||||||
<div className="w-20 h-20 rounded-2xl bg-stone-100 flex items-center justify-center mx-auto mb-6">
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-6">
|
||||||
<svg className="w-10 h-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
{brandName} · Summer 2026
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 className="text-2xl font-bold text-stone-800 mb-2">No Upcoming Stops Yet</h2>
|
|
||||||
<p className="text-stone-500 mb-8 max-w-md mx-auto">
|
|
||||||
The corn is still ripening in the field. New pickup stops are added every week — check back soon, or browse the full season schedule.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3">
|
<h1
|
||||||
<Link
|
className="font-display text-stone-50 text-[clamp(2.5rem,5.4vw,4.5rem)] leading-[1.02] tracking-[-0.02em]"
|
||||||
href="/tuxedo"
|
style={{ textWrap: "balance" }}
|
||||||
className="inline-flex items-center gap-2 rounded-2xl bg-stone-950 px-6 py-3 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-900 transition-colors"
|
>
|
||||||
>
|
Stops
|
||||||
Back to Tuxedo Corn
|
<br className="hidden sm:block" />{" "}
|
||||||
</Link>
|
<span className="italic text-amber-200/90">near you.</span>
|
||||||
|
</h1>
|
||||||
|
<div className="mt-8 h-px w-16 bg-gradient-to-r from-emerald-500 via-amber-400 to-amber-300/0" />
|
||||||
|
<p className="mt-8 text-stone-300 text-lg sm:text-xl leading-[1.65]">
|
||||||
|
Fresh Olathe Sweet™ sweet corn delivered to a stop
|
||||||
|
near you. Preorder for pickup, or just show up — we’ll
|
||||||
|
have coolers on the truck.
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 flex flex-wrap items-center gap-x-8 gap-y-3 text-[11px] uppercase tracking-[0.28em]">
|
||||||
|
<span className="text-amber-200/80">
|
||||||
|
{upcomingStops.length} upcoming
|
||||||
|
</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
|
<span className="text-stone-400">Updated weekly</span>
|
||||||
|
<span className="text-stone-700">·</span>
|
||||||
<Link
|
<Link
|
||||||
href="/api/tuxedo/schedule-pdf"
|
href="/api/tuxedo/schedule-pdf"
|
||||||
download
|
download
|
||||||
className="inline-flex items-center gap-2 rounded-2xl bg-white px-6 py-3 text-sm font-semibold text-stone-700 ring-1 ring-stone-200 hover:bg-stone-50 transition-colors"
|
className="text-stone-400 hover:text-amber-200/80 underline underline-offset-4 transition-colors"
|
||||||
>
|
>
|
||||||
Download Full Schedule
|
Download full schedule
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</LayoutContainer>
|
||||||
<div className="max-w-4xl mx-auto space-y-3">
|
</section>
|
||||||
{upcomingStops.map((stop) => (
|
|
||||||
<Link
|
|
||||||
key={stop.id}
|
|
||||||
href={`/${brandSlug}/stops/${stop.slug}`}
|
|
||||||
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-emerald-200"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 flex flex-col items-center justify-center text-white shadow-md shadow-emerald-500/20">
|
|
||||||
<span className="text-[10px] font-bold uppercase opacity-80">
|
|
||||||
{new Date(stop.date).toLocaleDateString("en-US", { month: "short" })}
|
|
||||||
</span>
|
|
||||||
<span className="text-xl font-black leading-none">
|
|
||||||
{new Date(stop.date).getDate()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
{/* Stops list — hairline-ruled magazine sidebar style. Each
|
||||||
<h3 className="text-lg font-bold text-stone-950 group-hover:text-emerald-700 transition-colors">
|
row is a single horizontal hairline + date numerals + city
|
||||||
{stop.city}, {stop.state}
|
+ location + time + chevron. No cards, no gradients. */}
|
||||||
</h3>
|
<section className="relative bg-stone-50 py-20 sm:py-24">
|
||||||
<p className="text-sm text-stone-500 truncate">{stop.location}</p>
|
<LayoutContainer>
|
||||||
{stop.address && (
|
{upcomingStops.length === 0 ? (
|
||||||
<p className="text-xs text-stone-400 truncate">{stop.address}</p>
|
<EmptyStops />
|
||||||
)}
|
) : (
|
||||||
</div>
|
<div className="mx-auto max-w-5xl">
|
||||||
|
{/* Eyebrow + hairline-ruled list header */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-baseline justify-between gap-4 mb-8">
|
||||||
<div className="text-right">
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700">
|
||||||
<p className="text-sm font-semibold text-stone-700">{stop.time}</p>
|
Upcoming
|
||||||
{stop.cutoff_time && (
|
</p>
|
||||||
<p className="text-xs text-stone-400">Order by {new Date(stop.cutoff_time).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" })}</p>
|
<p className="text-[10px] uppercase tracking-[0.3em] text-stone-400 tabular-nums">
|
||||||
)}
|
{upcomingStops.length} stops
|
||||||
</div>
|
</p>
|
||||||
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-emerald-50 flex items-center justify-center group-hover:bg-emerald-500 transition-colors">
|
|
||||||
<svg className="w-5 h-5 text-emerald-600 group-hover:text-white transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{pastStops.length > 0 && (
|
|
||||||
<div className="mt-8 pt-8 border-t border-stone-200">
|
|
||||||
<h2 className="text-sm font-bold uppercase tracking-widest text-stone-400 mb-4">Past Stops</h2>
|
|
||||||
<div className="space-y-2 opacity-60">
|
|
||||||
{pastStops.slice(0, 5).map((stop) => (
|
|
||||||
<div
|
|
||||||
key={stop.id}
|
|
||||||
className="flex items-center gap-4 p-4 rounded-xl bg-white/50 text-stone-500"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-stone-200 flex items-center justify-center text-xs font-bold text-stone-500">
|
|
||||||
{new Date(stop.date).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="font-medium">{stop.city}, {stop.state}</p>
|
|
||||||
<p className="text-xs">{stop.location}</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs">Completed</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<ol className="border-t border-stone-900/15">
|
||||||
)}
|
{upcomingStops.map((stop) => (
|
||||||
</LayoutContainer>
|
<StopRow key={stop.id} stop={stop} brandSlug={brandSlug} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
{pastStops.length > 0 && (
|
||||||
|
<div className="mt-20">
|
||||||
|
<div className="flex items-baseline justify-between gap-4 mb-8">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-[0.32em] text-stone-400">
|
||||||
|
Past Stops
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] uppercase tracking-[0.3em] text-stone-400 tabular-nums">
|
||||||
|
Last {Math.min(5, pastStops.length)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ol className="border-t border-stone-900/10 opacity-70">
|
||||||
|
{pastStops.slice(0, 5).map((stop) => (
|
||||||
|
<StopRow
|
||||||
|
key={stop.id}
|
||||||
|
stop={stop}
|
||||||
|
brandSlug={brandSlug}
|
||||||
|
past
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StorefrontFooter brandName={brandName} brandSlug={brandSlug} brandAccent="green" />
|
<StorefrontFooter brandName={brandName} brandSlug={brandSlug} brandAccent="green" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── STOP ROW — hairline-ruled magazine-sidebar cell ────────────────
|
||||||
|
// Date numerals on the left (large tabular day, small month above),
|
||||||
|
// city/state + location + time in the middle, chevron on the right.
|
||||||
|
// All hairline-divided rows; the list reads as a sidebar, not a feed.
|
||||||
|
function StopRow({
|
||||||
|
stop,
|
||||||
|
brandSlug,
|
||||||
|
past = false,
|
||||||
|
}: {
|
||||||
|
stop: PublicStop;
|
||||||
|
brandSlug: string;
|
||||||
|
past?: boolean;
|
||||||
|
}) {
|
||||||
|
const date = new Date(stop.date);
|
||||||
|
const day = date.getDate();
|
||||||
|
const month = date.toLocaleDateString("en-US", { month: "short" });
|
||||||
|
const weekday = date.toLocaleDateString("en-US", { weekday: "short" });
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div className="grid grid-cols-12 gap-4 sm:gap-6 items-center py-5 sm:py-6 group">
|
||||||
|
{/* Date column — big tabular day, small month/weekday above */}
|
||||||
|
<div className="col-span-3 sm:col-span-2">
|
||||||
|
<p
|
||||||
|
className={`text-[10px] uppercase tracking-[0.28em] tabular-nums ${
|
||||||
|
past ? "text-stone-400" : "text-emerald-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{weekday} · {month}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`mt-1 font-display tabular-nums leading-none tracking-[-0.02em] ${
|
||||||
|
past
|
||||||
|
? "text-stone-400 text-3xl sm:text-4xl"
|
||||||
|
: "text-stone-950 text-4xl sm:text-5xl"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* City + location */}
|
||||||
|
<div className="col-span-9 sm:col-span-6 min-w-0">
|
||||||
|
<h3
|
||||||
|
className={`font-display leading-[1.15] tracking-[-0.012em] ${
|
||||||
|
past
|
||||||
|
? "text-stone-500 text-base sm:text-lg"
|
||||||
|
: "text-stone-950 text-lg sm:text-xl group-hover:text-emerald-800 transition-colors"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stop.city}, {stop.state}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={`mt-1 truncate ${past ? "text-stone-400 text-sm" : "text-stone-500 text-sm"}`}
|
||||||
|
>
|
||||||
|
{stop.location}
|
||||||
|
</p>
|
||||||
|
{stop.address && (
|
||||||
|
<p
|
||||||
|
className={`mt-0.5 truncate ${past ? "text-stone-400 text-xs" : "text-stone-400 text-xs"}`}
|
||||||
|
>
|
||||||
|
{stop.address}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Time + cutoff */}
|
||||||
|
<div className="col-span-8 sm:col-span-3 text-right">
|
||||||
|
<p
|
||||||
|
className={`font-mono tabular-nums text-sm ${
|
||||||
|
past ? "text-stone-400" : "text-stone-950"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{stop.time}
|
||||||
|
</p>
|
||||||
|
{!past && stop.cutoff_time && (
|
||||||
|
<p className="mt-1 text-[10px] uppercase tracking-[0.2em] text-stone-400">
|
||||||
|
Order by{" "}
|
||||||
|
{new Date(stop.cutoff_time).toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{past && (
|
||||||
|
<p className="mt-1 text-[10px] uppercase tracking-[0.2em] text-stone-400">
|
||||||
|
Completed
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chevron — only on upcoming rows */}
|
||||||
|
<div className="hidden sm:flex col-span-1 justify-end">
|
||||||
|
{!past && (
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-stone-300 group-hover:text-emerald-700 group-hover:translate-x-1 transition-all"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<li className="stop-row border-b border-stone-900/15 last:border-b-0">
|
||||||
|
{content}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (past) {
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/${brandSlug}/stops/${stop.slug}`}
|
||||||
|
className="stop-row block border-b border-stone-900/15 last:border-b-0 hover:bg-stone-100/60 transition-colors"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── EMPTY STATE — editorial Fraunces italic copy ──────────────────
|
||||||
|
// Same "no stops yet" message as before, but rendered as a quiet
|
||||||
|
// editorial panel instead of an icon-in-rounded-box illustration.
|
||||||
|
function EmptyStops() {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-2xl text-center py-12">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
|
Coming Soon
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
className="font-display text-stone-950 text-[clamp(2rem,4.4vw,3.25rem)] leading-[1.05] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
The corn is still ripening
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-700/90">in the field.</span>
|
||||||
|
</h2>
|
||||||
|
<div className="mx-auto mt-7 h-px w-12 bg-emerald-700/60" />
|
||||||
|
<p className="mt-7 text-stone-600 text-lg leading-[1.65]">
|
||||||
|
New pickup stops are added every week. Check back soon, or browse
|
||||||
|
the full season schedule below.
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-3">
|
||||||
|
<Link
|
||||||
|
href="/tuxedo"
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-gradient-to-br from-emerald-700 to-emerald-600 px-8 py-3.5 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 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<span>Back to Tuxedo Corn</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}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/api/tuxedo/schedule-pdf"
|
||||||
|
download
|
||||||
|
className="inline-flex items-center gap-2 rounded-full bg-white px-8 py-3.5 text-sm font-bold text-stone-900 ring-1 ring-stone-200 hover:ring-emerald-600 hover:text-emerald-700 transition-all"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+257
-108
@@ -10,6 +10,7 @@ import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
|||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import { getStorefrontStopById } from "@/actions/storefront";
|
import { getStorefrontStopById } from "@/actions/storefront";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
import { FadeOnScroll } from "@/components/ui/ScrollAnimations";
|
||||||
|
|
||||||
type Product = {
|
type Product = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -31,30 +32,25 @@ type Stop = {
|
|||||||
time: string;
|
time: string;
|
||||||
location: string;
|
location: string;
|
||||||
brand_id: string;
|
brand_id: string;
|
||||||
|
cutoff_time?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
slug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function StopPage() {
|
const BRAND_NAME = "Tuxedo Corn";
|
||||||
|
const BRAND_SLUG = "tuxedo";
|
||||||
|
|
||||||
|
export default function TuxedoStopPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const slug = params.id as string;
|
const slug = params.id as string;
|
||||||
|
|
||||||
const [stop, setStop] = useState<{
|
const [stop, setStop] = useState<Stop | null>(null);
|
||||||
id: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
date: string;
|
|
||||||
time: string;
|
|
||||||
location: string;
|
|
||||||
brand_id: string;
|
|
||||||
} | null>(null);
|
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [brandSlug, setBrandSlug] = useState("tuxedo");
|
|
||||||
const [brandAccent, setBrandAccent] = useState<"green" | "orange" | "blue">("green");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
const result = await getStorefrontStopById(slug);
|
const result = await getStorefrontStopById(slug);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
setStop(result.stop as unknown as Stop);
|
setStop(result.stop as unknown as Stop);
|
||||||
setProducts(result.products as unknown as Product[]);
|
setProducts(result.products as unknown as Product[]);
|
||||||
}
|
}
|
||||||
@@ -64,38 +60,89 @@ export default function StopPage() {
|
|||||||
if (!stop) {
|
if (!stop) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
|
<StorefrontHeader
|
||||||
<main className="py-20">
|
brandName={BRAND_NAME}
|
||||||
|
brandSlug={BRAND_SLUG}
|
||||||
|
brandAccent="green"
|
||||||
|
/>
|
||||||
|
<main className="py-32">
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<div className="max-w-5xl mx-auto text-center">
|
<div className="mx-auto max-w-3xl text-center">
|
||||||
<h1 className="text-5xl font-black tracking-tight text-stone-950">Stop Not Found</h1>
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-700 mb-5">
|
||||||
|
Off the Route
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
className="font-display text-stone-950 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
That stop isn’t{" "}
|
||||||
|
<span className="italic text-amber-700/90">on the calendar.</span>
|
||||||
|
</h1>
|
||||||
|
<div className="mx-auto mt-7 h-px w-12 bg-emerald-700/60" />
|
||||||
|
<p className="mt-7 text-stone-600 leading-[1.65]">
|
||||||
|
It may have passed, or the URL is off by a character.
|
||||||
|
Browse the full list to find a stop near you.
|
||||||
|
</p>
|
||||||
|
<div className="mt-10">
|
||||||
|
<Link
|
||||||
|
href="/tuxedo/stops"
|
||||||
|
className="group inline-flex items-center gap-3 rounded-full bg-stone-950 px-8 py-3.5 text-sm font-bold text-white hover:bg-stone-800 transition-all"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transition-transform group-hover:-translate-x-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>All Stops</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
</main>
|
</main>
|
||||||
|
<StorefrontFooter
|
||||||
|
brandName={BRAND_NAME}
|
||||||
|
brandSlug={BRAND_SLUG}
|
||||||
|
brandAccent="green"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBlue = brandAccent === "blue";
|
const date = new Date(stop.date);
|
||||||
const brandLabel = isBlue ? "Indian River Direct" : "Tuxedo Corn";
|
const day = date.getDate();
|
||||||
|
const weekday = date.toLocaleDateString("en-US", { weekday: "long" });
|
||||||
|
const monthYear = date.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-stone-50">
|
<div className="min-h-screen bg-stone-50">
|
||||||
|
<StopSetEffect stop={stop} />
|
||||||
<StorefrontHeader
|
<StorefrontHeader
|
||||||
brandName={brandLabel}
|
brandName={BRAND_NAME}
|
||||||
brandSlug={brandSlug}
|
brandSlug={BRAND_SLUG}
|
||||||
brandAccent={brandAccent}
|
brandAccent="green"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="py-16 md:py-20">
|
<main>
|
||||||
<LayoutContainer>
|
{/* Back nav — kept slim, no white card wrapper. */}
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="bg-stone-50 border-b border-stone-200/60">
|
||||||
|
<LayoutContainer>
|
||||||
{/* Back navigation */}
|
<div className="py-5 flex items-center gap-2 text-sm">
|
||||||
<div className="mb-10 flex items-center gap-2">
|
|
||||||
<Link
|
<Link
|
||||||
href={`/${brandSlug}#stops`}
|
href={`/${BRAND_SLUG}#stops`}
|
||||||
className="flex items-center gap-2 text-sm font-medium text-stone-500 hover:text-stone-800 transition-colors group"
|
className="flex items-center gap-2 text-stone-500 hover:text-stone-950 transition-colors group"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="h-4 w-4 transition-transform duration-200 group-hover:-translate-x-1"
|
className="h-4 w-4 transition-transform duration-200 group-hover:-translate-x-1"
|
||||||
@@ -103,89 +150,164 @@ export default function StopPage() {
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
All Stops
|
All Stops
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-stone-300">/</span>
|
<span className="text-stone-300">/</span>
|
||||||
<span className="text-sm text-stone-700 font-medium">{stop.city}, {stop.state}</span>
|
<span className="text-stone-700 font-medium truncate">
|
||||||
|
{stop.city}, {stop.state}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Stop header */}
|
{/* Hero — dark editorial opener. Eyebrow + huge Fraunces
|
||||||
<div className="mb-12">
|
city/state headline with the date numerals set as part of
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">
|
the type composition, not as a separate stat block. */}
|
||||||
{brandLabel}
|
<section className="relative bg-stone-950 overflow-hidden isolate">
|
||||||
</p>
|
<div
|
||||||
<h1 className="text-5xl md:text-7xl font-black tracking-tight text-stone-950 leading-[1.0]">
|
aria-hidden="true"
|
||||||
{stop.city},<br className="hidden md:block" /> {stop.state}
|
className="absolute inset-x-0 bottom-0 h-px bg-amber-300/30 z-10"
|
||||||
</h1>
|
/>
|
||||||
<p className="mt-5 max-w-xl text-lg text-stone-500 leading-relaxed">
|
|
||||||
{isBlue
|
|
||||||
? "Order fresh Florida citrus for pickup at this stop."
|
|
||||||
: "Order fresh Olathe Sweet™ sweet corn for pickup at this stop."}
|
|
||||||
</p>
|
|
||||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Stop info */}
|
<LayoutContainer>
|
||||||
<div className="rounded-3xl bg-white p-8 mb-14 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-xl hover:ring-stone-300/60">
|
<div className="relative py-20 sm:py-28 lg:py-32 grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-end">
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
{/* Left: copy + headline */}
|
||||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
<div className="lg:col-span-8">
|
||||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
<FadeOnScroll from="up">
|
||||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-amber-300/85 mb-6">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
Pickup Stop · {monthYear}
|
||||||
</svg>
|
</p>
|
||||||
</div>
|
</FadeOnScroll>
|
||||||
<div>
|
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Date</p>
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
<p className="font-bold text-stone-950">{formatDate(stop.date)}</p>
|
<h1
|
||||||
</div>
|
className="font-display text-stone-50 text-[clamp(2.75rem,7vw,5.5rem)] leading-[0.98] tracking-[-0.025em]"
|
||||||
</div>
|
style={{ textWrap: "balance" }}
|
||||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
>
|
||||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
{stop.city},
|
||||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<br className="hidden sm:block" />{" "}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<span className="italic text-amber-200/90">
|
||||||
</svg>
|
{stop.state}.
|
||||||
</div>
|
</span>
|
||||||
<div>
|
</h1>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Time</p>
|
</FadeOnScroll>
|
||||||
<p className="font-bold text-stone-950">{stop.time}</p>
|
|
||||||
</div>
|
<FadeOnScroll from="up" delay={0.18}>
|
||||||
</div>
|
<div className="mt-8 h-px w-16 bg-gradient-to-r from-emerald-500 via-amber-400 to-amber-300/0" />
|
||||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
</FadeOnScroll>
|
||||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
|
||||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<FadeOnScroll from="up" delay={0.26}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
<p className="mt-8 text-stone-300 text-lg sm:text-xl leading-[1.65] max-w-xl">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
Order fresh Olathe Sweet™ sweet corn for pickup
|
||||||
</svg>
|
at this stop. We’ll have it on the truck the
|
||||||
</div>
|
morning of — ready when you are.
|
||||||
<div>
|
</p>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Location</p>
|
</FadeOnScroll>
|
||||||
<p className="font-bold text-stone-950 leading-tight">{stop.location}</p>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
{/* Right: date as a big editorial stamp. */}
|
||||||
|
<div className="lg:col-span-4 lg:text-right">
|
||||||
|
<FadeOnScroll from="right">
|
||||||
|
<p className="text-[10px] uppercase tracking-[0.32em] text-amber-300/70 mb-2 tabular-nums">
|
||||||
|
{weekday}
|
||||||
|
</p>
|
||||||
|
<p className="font-display tabular-nums text-stone-50 text-[clamp(4rem,8vw,6.5rem)] leading-none tracking-[-0.03em]">
|
||||||
|
{day}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[11px] uppercase tracking-[0.28em] text-stone-400 tabular-nums">
|
||||||
|
{date.toLocaleDateString("en-US", {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Stop info — hairline-ruled four-column data grid. Date /
|
||||||
|
Time / Location / Order by. Same magazine-sidebar register
|
||||||
|
as the homepage's HarvestEditorial numbers strip. */}
|
||||||
|
<section className="relative bg-stone-50 py-16 sm:py-20 border-b border-stone-200/60">
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="mx-auto max-w-5xl">
|
||||||
|
<ol className="grid grid-cols-2 lg:grid-cols-4 gap-y-10 lg:gap-y-0 lg:gap-x-10 border-t border-b border-stone-900/15">
|
||||||
|
<StopInfoPoint label="Date" detail={formatDate(stop.date)} />
|
||||||
|
<StopInfoPoint label="Time" detail={stop.time} mono />
|
||||||
|
<StopInfoPoint label="Location" detail={stop.location} />
|
||||||
|
<StopInfoPoint
|
||||||
|
label="Order By"
|
||||||
|
detail={
|
||||||
|
stop.cutoff_time
|
||||||
|
? new Date(stop.cutoff_time).toLocaleTimeString(
|
||||||
|
"en-US",
|
||||||
|
{ hour: "numeric", minute: "2-digit" }
|
||||||
|
)
|
||||||
|
: "Day-of"
|
||||||
|
}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</LayoutContainer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Available Products — cream section with editorial framing,
|
||||||
|
same register as the homepage ProductsSection header. */}
|
||||||
|
<section className="relative bg-stone-50 py-24 sm:py-32">
|
||||||
|
<LayoutContainer>
|
||||||
|
<div className="mx-auto max-w-5xl">
|
||||||
|
<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">
|
||||||
|
<FadeOnScroll from="up">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-[0.32em] text-emerald-700 mb-5">
|
||||||
|
Farm-Direct · This Stop
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.08}>
|
||||||
|
<h2
|
||||||
|
className="font-display text-stone-950 text-[clamp(2rem,4.4vw,3.5rem)] leading-[1.04] tracking-[-0.02em]"
|
||||||
|
style={{ textWrap: "balance" }}
|
||||||
|
>
|
||||||
|
Preorder
|
||||||
|
<br className="hidden sm:block" />{" "}
|
||||||
|
<span className="italic text-amber-700/90">
|
||||||
|
for this stop.
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</FadeOnScroll>
|
||||||
|
<FadeOnScroll from="up" delay={0.16}>
|
||||||
|
<div className="mt-7 h-px w-12 bg-emerald-700/60" />
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-5">
|
||||||
|
<FadeOnScroll from="up" delay={0.2}>
|
||||||
|
<p className="text-stone-600 text-base leading-[1.7] lg:max-w-md lg:ml-auto">
|
||||||
|
Add items below and we’ll have them ready at
|
||||||
|
the stop. Preordering helps us bring the right
|
||||||
|
amount of corn to each pickup.
|
||||||
|
</p>
|
||||||
|
</FadeOnScroll>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{/* Available Products — editorial header */}
|
|
||||||
<section>
|
|
||||||
<div className="mb-10">
|
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
|
||||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-tight">
|
|
||||||
Available Products
|
|
||||||
</h2>
|
|
||||||
<div className="mt-5 h-px w-12 bg-emerald-600" />
|
|
||||||
<p className="mt-5 text-base text-stone-500 leading-relaxed">
|
|
||||||
Preorder for pickup — add items below and we will have them ready at the stop.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{products.length === 0 ? (
|
{products.length === 0 ? (
|
||||||
<div className="rounded-3xl bg-white p-12 text-center shadow-sm ring-1 ring-stone-200/60">
|
<div className="border-t border-b border-stone-900/15 py-16 text-center">
|
||||||
<p className="text-stone-500">No products available for this stop.</p>
|
<p className="text-stone-500">
|
||||||
|
No products available for this stop yet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-8 md:grid-cols-3">
|
<div className="grid gap-x-8 gap-y-12 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={product.id}
|
key={product.id}
|
||||||
@@ -195,26 +317,53 @@ export default function StopPage() {
|
|||||||
price={`$${product.price}`}
|
price={`$${product.price}`}
|
||||||
type={product.type}
|
type={product.type}
|
||||||
imageUrl={product.image_url}
|
imageUrl={product.image_url}
|
||||||
brandSlug={brandSlug}
|
brandSlug={BRAND_SLUG}
|
||||||
brandName={brandLabel}
|
brandName={BRAND_NAME}
|
||||||
brandId={product.brand_id}
|
brandId={product.brand_id}
|
||||||
brandAccent={brandAccent}
|
brandAccent="green"
|
||||||
is_taxable={product.is_taxable}
|
is_taxable={product.is_taxable}
|
||||||
pickup_type={product.pickup_type}
|
pickup_type={product.pickup_type}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</LayoutContainer>
|
||||||
</LayoutContainer>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<StorefrontFooter
|
<StorefrontFooter
|
||||||
brandName={brandLabel}
|
brandName={BRAND_NAME}
|
||||||
brandSlug={brandSlug}
|
brandSlug={BRAND_SLUG}
|
||||||
brandAccent={brandAccent}
|
brandAccent="green"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One column of the stop-info data strip — same hairline + label
|
||||||
|
// treatment as the homepage's HarvestEditorial DataPoint.
|
||||||
|
function StopInfoPoint({
|
||||||
|
label,
|
||||||
|
detail,
|
||||||
|
mono = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
detail: string;
|
||||||
|
mono?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="relative pt-8 lg:pt-12">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-[0.3em] text-emerald-700">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className={`mt-4 text-stone-950 text-base leading-[1.55] ${
|
||||||
|
mono ? "font-mono tabular-nums tracking-tight" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{detail}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
|
||||||
import { m as motion } from "framer-motion";
|
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
|
||||||
|
|
||||||
export default function CTASection() {
|
|
||||||
return (
|
|
||||||
<section className="py-20 bg-white relative">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-stone-100 to-transparent" />
|
|
||||||
|
|
||||||
<LayoutContainer>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="relative text-center"
|
|
||||||
>
|
|
||||||
<h2 className="text-3xl md:text-4xl font-black tracking-tight text-stone-950">Ready to Order?</h2>
|
|
||||||
<p className="mt-4 text-stone-500 text-lg">Experience the difference four decades of excellence makes.</p>
|
|
||||||
|
|
||||||
<div className="mt-10 flex flex-wrap justify-center gap-4">
|
|
||||||
<Link
|
|
||||||
href="/tuxedo#stops"
|
|
||||||
className="group relative inline-flex items-center gap-3 rounded-full bg-stone-900 px-8 py-4 font-bold text-white overflow-hidden"
|
|
||||||
>
|
|
||||||
<span className="absolute inset-0 bg-gradient-to-r from-emerald-700 to-emerald-600 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
||||||
<span className="relative flex items-center gap-2">
|
|
||||||
Find a Stop
|
|
||||||
<svg className="w-5 h-5 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/tuxedo#products"
|
|
||||||
className="group relative inline-flex items-center gap-3 rounded-full bg-white px-8 py-4 font-bold text-stone-900 ring-2 ring-stone-200 hover:ring-emerald-600 hover:text-emerald-700 transition-all"
|
|
||||||
>
|
|
||||||
<span className="relative">
|
|
||||||
Shop Products
|
|
||||||
<svg className="inline w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</LayoutContainer>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { m as motion } from "framer-motion";
|
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
|
||||||
|
|
||||||
interface ContactSectionProps {
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ContactSection({ address }: ContactSectionProps) {
|
|
||||||
return (
|
|
||||||
<section className="py-24 md:py-32 bg-stone-950 text-white relative overflow-hidden">
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<div className="absolute top-0 left-1/3 w-96 h-96 bg-emerald-900/20 rounded-full blur-3xl" />
|
|
||||||
<div className="absolute bottom-0 right-1/3 w-80 h-80 bg-amber-900/10 rounded-full blur-3xl" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<LayoutContainer>
|
|
||||||
<div className="relative">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center mb-16"
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-bold uppercase tracking-[0.25em] text-emerald-400/60">Get in Touch</span>
|
|
||||||
<h2 className="mt-4 text-4xl md:text-5xl font-black tracking-tight text-white leading-tight">
|
|
||||||
Visit Our Farm
|
|
||||||
</h2>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-12 max-w-4xl mx-auto">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.1, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center group"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-stone-800 flex items-center justify-center mx-auto mb-6 group-hover:bg-emerald-900 transition-colors">
|
|
||||||
<svg className="w-7 h-7 text-stone-400 group-hover:text-emerald-400 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] font-bold uppercase tracking-widest text-stone-500 mb-3">Address</p>
|
|
||||||
<p className="text-stone-300 leading-relaxed">{address}</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.2, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center group"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-stone-800 flex items-center justify-center mx-auto mb-6 group-hover:bg-emerald-900 transition-colors">
|
|
||||||
<svg className="w-7 h-7 text-stone-400 group-hover:text-emerald-400 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] font-bold uppercase tracking-widest text-stone-500 mb-3">Phone</p>
|
|
||||||
<p className="text-stone-300">Shipping: 970-323-5631</p>
|
|
||||||
<p className="text-stone-300 mt-1">Office: 970-323-6874</p>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.3, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center group"
|
|
||||||
>
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-stone-800 flex items-center justify-center mx-auto mb-6 group-hover:bg-emerald-900 transition-colors">
|
|
||||||
<svg className="w-7 h-7 text-stone-400 group-hover:text-emerald-400 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] font-bold uppercase tracking-widest text-stone-500 mb-3">Hours</p>
|
|
||||||
<p className="text-stone-300">M–F: 7AM–6PM</p>
|
|
||||||
<p className="text-stone-300 mt-1">Sat & Sun: 8AM–2PM</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</LayoutContainer>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { m as motion } from "framer-motion";
|
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
|
||||||
|
|
||||||
const FAMILY_MEMBERS = [
|
|
||||||
{
|
|
||||||
initials: "JH",
|
|
||||||
name: "John Harold",
|
|
||||||
role: "Founder",
|
|
||||||
bio: "John Harold began selling Olathe's sweet corn directly from pickups in the 1970s. With unwavering dedication to community, family, and agriculture, he built Tuxedo Corn into a thriving family operation. Prior to farming, John established the first Group Home on the Western Slope and opened Colorow Care Center in Montrose County.",
|
|
||||||
era: "1970s",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initials: "DH",
|
|
||||||
name: "David Harold",
|
|
||||||
role: "Second Generation",
|
|
||||||
bio: "David leads sustainability efforts across Harold farms, implementing drip irrigation and joining the Fair Food Program to ensure responsible labor practices. He continues the family's commitment to raising top-quality corn while protecting the land for future generations.",
|
|
||||||
era: "Present",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initials: "JW",
|
|
||||||
name: "John William Harold",
|
|
||||||
role: "Mexico Operations",
|
|
||||||
bio: "John William has worked in Guaymas, Sonora, Mexico for over three decades growing sweet corn, onions, wheat, and vegetables. His international operations extend the Harold family farming tradition across borders.",
|
|
||||||
era: "1990s–Present",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initials: "JH",
|
|
||||||
name: "Joseph Harold",
|
|
||||||
role: "Third Generation",
|
|
||||||
bio: "Joseph works at the corn shed during summer and handles photography and beverages, carrying the family tradition into the next generation while learning every aspect of the business.",
|
|
||||||
era: "Present",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Family member card with hover effects
|
|
||||||
function FamilyCard({ member, index }: { member: typeof FAMILY_MEMBERS[0]; index: number }) {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 32 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-60px" }}
|
|
||||||
transition={{ duration: 0.6, delay: index * 0.12, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
whileHover={{ y: -6 }}
|
|
||||||
className="group relative"
|
|
||||||
>
|
|
||||||
{/* Era badge */}
|
|
||||||
<div className="absolute -top-3 -left-3 z-10">
|
|
||||||
<span className="inline-block bg-stone-900 text-stone-200 text-[10px] font-bold uppercase tracking-widest px-3 py-1.5 rounded-full shadow-lg">
|
|
||||||
{member.era}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative bg-gradient-to-b from-white to-stone-50 rounded-3xl p-8 shadow-lg shadow-black/5 ring-1 ring-stone-200/40 overflow-hidden h-full">
|
|
||||||
{/* Decorative top line */}
|
|
||||||
<motion.div
|
|
||||||
initial={{ scaleX: 0 }}
|
|
||||||
whileHover={{ scaleX: 1 }}
|
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
|
||||||
className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-emerald-600 via-emerald-400 to-amber-500 origin-left"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Avatar */}
|
|
||||||
<div className="mb-6 relative">
|
|
||||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-stone-900 to-stone-800 flex items-center justify-center shadow-xl shadow-stone-900/30">
|
|
||||||
<span className="text-2xl font-black text-white tracking-tight">{member.initials}</span>
|
|
||||||
</div>
|
|
||||||
{/* Decorative corner accent */}
|
|
||||||
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-emerald-600/20 rounded-full blur-sm" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-black text-stone-950 tracking-tight">{member.name}</h3>
|
|
||||||
<p className="mt-2 text-[11px] font-bold uppercase tracking-widest text-emerald-700">{member.role}</p>
|
|
||||||
<p className="mt-4 text-sm leading-relaxed text-stone-600">{member.bio}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom accent */}
|
|
||||||
<div className="absolute bottom-0 left-8 right-8 h-px bg-gradient-to-r from-transparent via-stone-200 to-transparent" />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FamilyTimelineSection() {
|
|
||||||
return (
|
|
||||||
<section className="py-24 md:py-32 bg-stone-100 relative">
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(16,100,50,0.08)_0%,transparent_50%)]" />
|
|
||||||
|
|
||||||
<LayoutContainer>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center mb-20"
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-bold uppercase tracking-[0.25em] text-emerald-700">The People Behind the Corn</span>
|
|
||||||
<h2 className="mt-4 text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-tight">
|
|
||||||
The Harold Family
|
|
||||||
</h2>
|
|
||||||
<div className="mt-8 mx-auto h-px w-24 bg-gradient-to-r from-emerald-600 via-emerald-400 to-transparent" />
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
|
||||||
{FAMILY_MEMBERS.map((member, index) => (
|
|
||||||
<FamilyCard key={member.name} member={member} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</LayoutContainer>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { m as motion } from "framer-motion";
|
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
|
||||||
|
|
||||||
export default function MissionSection() {
|
|
||||||
return (
|
|
||||||
<section className="py-24 md:py-32 bg-stone-50 relative overflow-hidden">
|
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-emerald-100/30 rounded-full blur-3xl -z-10" />
|
|
||||||
|
|
||||||
<LayoutContainer>
|
|
||||||
<div className="mx-auto max-w-4xl">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="text-center mb-16"
|
|
||||||
>
|
|
||||||
<span className="text-[11px] font-bold uppercase tracking-[0.25em] text-emerald-700">Our Mission</span>
|
|
||||||
<h2 className="mt-4 text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-tight">
|
|
||||||
Raising the Best Corn<br />in the World
|
|
||||||
</h2>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-12">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.15, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="prose prose-lg"
|
|
||||||
>
|
|
||||||
<p className="text-stone-600 leading-relaxed text-lg">
|
|
||||||
Tuxedo Corn is the <strong className="text-stone-900">exclusive grower and shipper</strong> of Olathe Sweet Sweet Corn — the only corn that carries that prestigious name. When you order from us, you receive the real thing: grown by the only family that planted roots in Olathe, Colorado over forty years ago.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.25, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="prose prose-lg"
|
|
||||||
>
|
|
||||||
<p className="text-stone-600 leading-relaxed text-lg">
|
|
||||||
Not all sweet corn is equal. Olathe Sweet was developed for our high-altitude mountain climate, where <strong className="text-stone-900">intense summer sun by day meets cool mountain air at night</strong>. That combination coaxes extraordinary sweetness and tenderness out of every single ear.
|
|
||||||
</p>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 48 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true, margin: "-80px" }}
|
|
||||||
transition={{ duration: 0.8, delay: 0.35, ease: [0.22, 0.61, 0.36, 1] }}
|
|
||||||
className="mt-16 bg-white rounded-3xl p-10 shadow-lg shadow-stone-900/5 ring-1 ring-stone-200/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-6">
|
|
||||||
<div className="w-14 h-14 rounded-2xl bg-emerald-100 flex items-center justify-center flex-shrink-0">
|
|
||||||
<svg className="w-7 h-7 text-emerald-700" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-bold text-stone-950 mb-3">Grown with Future Generations in Mind</h3>
|
|
||||||
<p className="text-stone-600 leading-relaxed">
|
|
||||||
We farm regeneratively — no-till and minimal-till methods protect soil structure, cover crops naturally cycle nutrients, and reduced chemical inputs keep our mountain water pure. Every ear is hand-picked at peak freshness, inspected by our dedicated crew. Not machine-harvested. Not sorted by strangers. <strong className="text-stone-900">Hand-picked.</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</LayoutContainer>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 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.)
|
||||||
|
*
|
||||||
|
* Lifted out of `src/app/tuxedo/page.tsx` so the about page, FAQ page,
|
||||||
|
* and any future Tuxedo subpage can share the same library without
|
||||||
|
* duplicating the URL list.
|
||||||
|
*/
|
||||||
|
export const TUXEDO_IMAGES = {
|
||||||
|
/** Wide panoramic corn row with irrigation canal + Colorado sky — the
|
||||||
|
* hero poster on the homepage. */
|
||||||
|
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 on the homepage + about. */
|
||||||
|
stormField:
|
||||||
|
"https://s3.crispygoat.com/videos/wp-import/images/tuxedo-field-storm.jpg",
|
||||||
|
/** White-haired founder inspecting tassels — editorial-graded version
|
||||||
|
* of the original WP shot. Golden-hour lighting, shallow DOF. */
|
||||||
|
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. */
|
||||||
|
piledCobs:
|
||||||
|
"https://s3.crispygoat.com/videos/wp-import/images/corn-field-zane-original.jpg",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type TuxedoImageKey = keyof typeof TUXEDO_IMAGES;
|
||||||
Reference in New Issue
Block a user