Initial commit - Route Commerce platform

This commit is contained in:
2026-06-01 19:40:55 +00:00
commit 53a9671461
617 changed files with 106132 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
"use client";
import { useEffect, useState, Suspense, lazy } from "react";
import { motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import { supabase } from "@/lib/supabase";
import { getBrandSettingsPublic } from "@/actions/brand-settings";
// Lazy load heavy sections
const MissionSection = lazy(() => import("@/components/about/MissionSection"));
const FamilyTimelineSection = lazy(() => import("@/components/about/FamilyTimelineSection"));
const ContactSection = lazy(() => import("@/components/about/ContactSection"));
const CTASection = lazy(() => import("@/components/about/CTASection"));
const OLATHE_SWEET_LOGO_DARK =
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png";
export default function TuxedoAboutPage() {
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
const [olatheSweetLogoUrlDark, setOlatheSweetLogoUrlDark] = useState<string | null>(null);
const [aboutHeadline, setAboutHeadline] = useState<string | null>(null);
const [aboutSubheadline, setAboutSubheadline] = useState<string | null>(null);
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
const [contactEmail, setContactEmail] = useState<string | null>(null);
const [contactPhone, setContactPhone] = useState<string | null>(null);
const [address, setAddress] = useState<string>("59751 David Road, Olathe, CO 81425");
useEffect(() => {
async function load() {
const settingsResult = await getBrandSettingsPublic("tuxedo");
if (settingsResult.success && settingsResult.settings) {
const s = settingsResult.settings;
setLogoUrl(s.logo_url ?? null);
setLogoUrlDark(s.logo_url_dark ?? null);
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? null);
setAboutHeadline(s.about_headline ?? null);
setAboutSubheadline(s.about_subheadline ?? null);
setCustomFooterText(s.custom_footer_text ?? null);
setContactEmail(s.email ?? null);
setContactPhone(s.phone ?? null);
setShowWholesaleLink(s.show_wholesale_link ?? true);
if (s.brand_id) {
const { data: ws } = await supabase
.from("wholesale_settings")
.select("invoice_business_address")
.eq("brand_id", s.brand_id)
.single();
if (ws?.invoice_business_address) setAddress(ws.invoice_business_address);
}
}
try {
const { getCurrentAdminUser } = await import("@/actions/admin-user");
setIsAdmin(!!await getCurrentAdminUser());
} catch { /* not logged in */ }
}
load();
}, []);
return (
<div className="min-h-screen bg-stone-50">
<StorefrontHeader
brandName="Tuxedo Corn"
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
showWholesaleLink={showWholesaleLink}
isAdmin={isAdmin}
brandAccent="green"
/>
<main>
{/* Hero Section */}
<section className="relative bg-stone-950 overflow-hidden">
{/* Decorative elements */}
<div className="absolute inset-0 opacity-20">
<div className="absolute top-0 left-1/4 w-96 h-96 bg-emerald-900/20 rounded-full blur-3xl" />
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-amber-900/10 rounded-full blur-3xl" />
</div>
{/* Pattern overlay */}
<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">
<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
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
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
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 */}
<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" />
<img
src={olatheSweetLogoUrlDark ?? OLATHE_SWEET_LOGO_DARK}
alt="Olathe Sweet"
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>
<StorefrontFooter
brandName="Tuxedo Corn"
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
customFooterText={customFooterText}
contactEmail={contactEmail}
contactPhone={contactPhone}
isAdmin={isAdmin}
brandAccent="green"
/>
</div>
);
}
// Simple loading fallback - no more skeleton spinners
function LoadingFallback({ bg }: { bg: string }) {
return <div className={`py-24 md:py-32 ${bg}`} />;
}
+228
View File
@@ -0,0 +1,228 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
import { supabase } from "@/lib/supabase";
type BrandSettings = {
invoice_business_name: string | null;
invoice_business_address: string | null;
invoice_business_phone: string | null;
invoice_business_email: string | null;
invoice_business_website: string | null;
};
type FormState = {
name: string;
email: string;
topic: string;
message: string;
};
export default function TuxedoContactPage() {
const [brandSettings, setBrandSettings] = useState<BrandSettings | null>(null);
const [submitted, setSubmitted] = useState(false);
const [form, setForm] = useState<FormState>({ name: "", email: "", topic: "general", message: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
supabase
.from("wholesale_settings")
.select("invoice_business_name, invoice_business_address, invoice_business_phone, invoice_business_email, invoice_business_website")
.eq("brand_id", TUXEDO_BRAND_ID)
.single()
.then(({ data }) => setBrandSettings(data ?? null));
}, []);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setTimeout(() => {
setSubmitted(true);
setIsSubmitting(false);
}, 600);
}
return (
<div className="min-h-screen bg-stone-50">
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
<main className="py-16 md:py-20">
<LayoutContainer>
<div className="max-w-5xl mx-auto">
{/* Page header */}
<div className="text-center mb-16">
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Get in Touch</p>
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">
Contact Us
</h1>
<p className="mt-5 text-lg text-stone-500 max-w-xl mx-auto leading-relaxed">
Questions about corn, stops, or wholesale accounts we are happy to help.
</p>
</div>
{/* Contact info grid */}
<div className="grid gap-6 md:grid-cols-3 mb-16">
<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="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>
<h3 className="text-base font-bold text-stone-950 mb-3">Farm Address</h3>
<p className="text-sm text-stone-500 leading-relaxed">
{brandSettings?.invoice_business_address ?? "59751 David Road, Olathe, CO 81425"}
</p>
</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="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" />
</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
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
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div>
<label className="block text-sm font-semibold text-stone-700 mb-2">Your Name</label>
<input
required
value={form.name}
onChange={(e) => setForm({ ...form, name: 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"
placeholder="Jane Smith"
/>
</div>
<div>
<label className="block text-sm font-semibold text-stone-700 mb-2">Email</label>
<input
required
type="email"
value={form.email}
onChange={(e) => setForm({ ...form, email: 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"
placeholder="jane@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-stone-700 mb-2">Topic</label>
<select
value={form.topic}
onChange={(e) => setForm({ ...form, topic: e.target.value })}
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"
>
<option value="general">General Question</option>
<option value="orders">Orders & Pickup</option>
<option value="wholesale">Wholesale Inquiry</option>
<option value="media">Media & Partnership</option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-stone-700 mb-2">Message</label>
<textarea
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
type="submit"
disabled={isSubmitting}
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"
>
{isSubmitting ? "Sending..." : "Send Message"}
</button>
</form>
)}
</div>
{/* Business hours — elevated */}
<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>
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
"use client";
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<main className="min-h-screen bg-red-50 px-6 py-12">
<div className="mx-auto max-w-2xl">
<h1 className="text-3xl font-bold text-red-900">Page Error</h1>
<p className="mt-4 text-red-700">{error.message}</p>
{error.digest && (
<p className="mt-2 text-sm text-red-500">Digest: {error.digest}</p>
)}
<button
onClick={reset}
className="mt-6 rounded-xl bg-red-600 px-4 py-2 text-white"
>
Try again
</button>
</div>
</main>
);
}
+234
View File
@@ -0,0 +1,234 @@
"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
type FAQItem = {
question: string;
answer: string;
};
const FAQ_CATEGORIES = [
{
category: "Ordering & Pickup",
items: [
{
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.",
},
{
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.",
},
{
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.",
},
],
},
{
category: "Shipping & Delivery",
items: [
{
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.",
},
{
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.",
},
{
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.",
},
],
},
{
category: "Wholesale",
items: [
{
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.",
},
{
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.",
},
],
},
{
category: "Product & Quality",
items: [
{
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.",
},
{
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.",
},
{
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.",
},
],
},
];
function FAQAccordion({ items, searchQuery }: { items: FAQItem[]; searchQuery: string }) {
const [open, setOpen] = useState<string | null>(null);
const filtered = useMemo(() => {
if (!searchQuery.trim()) return items;
const q = searchQuery.toLowerCase();
return items.filter(
(item) =>
item.question.toLowerCase().includes(q) ||
item.answer.toLowerCase().includes(q)
);
}, [items, searchQuery]);
if (filtered.length === 0) {
return (
<div className="rounded-2xl bg-white p-8 text-center shadow-sm ring-1 ring-stone-200/60">
<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 all categories below.</p>
</div>
);
}
return (
<div className="space-y-2">
{filtered.map((item) => {
const isOpen = open === item.question;
return (
<div
key={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"
>
<button
onClick={() => setOpen(isOpen ? null : item.question)}
className="flex w-full items-center justify-between px-6 py-5 text-left group"
aria-expanded={isOpen}
>
<span className="font-semibold text-stone-950 pr-4 text-[15px] leading-snug group-hover:text-emerald-700 transition-colors">
{item.question}
</span>
<span
className={`flex-shrink-0 w-7 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"
}`}
>
<svg
className="w-3.5 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>
</span>
</button>
<div
className={`overflow-hidden transition-all duration-300 ease-out ${
isOpen ? "max-h-48 opacity-100" : "max-h-0 opacity-0"
}`}
>
<div className="px-6 pb-6 pt-1 border-t border-stone-100">
<p className="text-sm text-stone-500 leading-relaxed">{item.answer}</p>
</div>
</div>
</div>
);
})}
</div>
);
}
export default function TuxedoFAQPage() {
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="min-h-screen bg-stone-50">
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
<main className="py-16 md:py-20">
<LayoutContainer>
<div className="max-w-3xl mx-auto">
{/* Page header */}
<div className="text-center mb-14">
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Questions</p>
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">
FAQ
</h1>
<p className="mt-5 text-lg text-stone-500 leading-relaxed">
Everything you need to know about ordering, pickup, and shipping.
</p>
</div>
{/* Search */}
<div className="mb-12 relative">
<div className="absolute inset-y-0 left-5 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}>
<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
type="text"
placeholder="Search questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded-2xl border border-stone-200 bg-white pl-11 pr-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 shadow-sm"
/>
{searchQuery && (
<button
onClick={() => setSearchQuery("")}
className="absolute inset-y-0 right-4 flex items-center text-stone-400 hover:text-stone-600 transition-colors"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* FAQ categories */}
<div className="space-y-14">
{FAQ_CATEGORIES.map((cat) => (
<div key={cat.category}>
<h2 className="text-xs font-bold uppercase tracking-[0.2em] text-stone-400 mb-5">
{cat.category}
</h2>
<FAQAccordion items={cat.items} searchQuery={searchQuery} />
</div>
))}
</div>
{/* Still have questions CTA */}
<div className="mt-16 rounded-3xl bg-stone-950 px-10 py-12 text-center shadow-xl">
<p className="text-2xl font-black text-white tracking-tight">Still have questions?</p>
<p className="mt-2 text-stone-400 text-sm">We are happy to help reach out anytime.</p>
<Link
href="/tuxedo/contact"
className="mt-8 inline-flex items-center gap-2.5 rounded-full bg-emerald-600 px-8 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"
>
Contact Us
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</div>
</LayoutContainer>
</main>
<StorefrontFooter brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Tuxedo Corn | Fresh Produce Wholesale",
description: "Premium sweet corn and seasonal produce delivered fresh from the farm to pickup stops near you. Shop wholesale pricing on Tuxedo Corn.",
openGraph: {
title: "Tuxedo Corn",
description: "Premium sweet corn and seasonal produce, delivered fresh from our farm to your community.",
siteName: "Route Commerce",
},
};
export default function TuxedoLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen relative">
{/* Background gradient */}
<div className="fixed inset-0 bg-gradient-to-br from-emerald-950/50 via-zinc-950 to-zinc-950" />
<div className="fixed inset-0 pointer-events-none">
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-emerald-500/10 rounded-full blur-[120px]" />
</div>
<div className="relative">{children}</div>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
export default function Loading() {
return (
<main className="min-h-screen bg-stone-50 px-6 py-12">
<div className="mx-auto max-w-5xl">
<div className="animate-pulse space-y-6">
<div className="h-6 w-32 rounded bg-stone-200" />
<div className="h-12 w-72 rounded bg-stone-200" />
<div className="h-4 w-96 rounded bg-stone-200" />
<div className="mt-10 grid gap-6 md:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="h-48 rounded-2xl bg-stone-200" />
))}
</div>
</div>
</div>
</main>
);
}
+691
View File
@@ -0,0 +1,691 @@
"use client";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { motion, useInView } from "framer-motion";
import { useCart } from "@/context/CartContext";
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
import LayoutContainer from "@/components/layout/LayoutContainer";
import ProductCard from "@/components/storefront/ProductCard";
import PaginatedStops from "@/components/storefront/PaginatedStops";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
import { supabase } from "@/lib/supabase";
import { getBrandSettingsPublic } from "@/actions/brand-settings";
type Brand = {
id: string;
name: string;
slug: string;
};
type Stop = {
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
slug: string;
brand_id: string;
};
type Product = {
id: string;
name: string;
description: string | null;
price: number;
type: string;
image_url: string | null;
brand_id: string;
is_taxable?: boolean;
pickup_type?: "scheduled_stop" | "shed";
};
// ── Why Choose Tuxedo Corn — editorial masonry redesign ─────────────
type FeatureColor = "emerald" | "amber";
interface Feature {
color: FeatureColor;
label: string;
headline: string;
story: string;
accentColor: string;
accentGlow: string;
accentHover: string;
size: "tall" | "normal";
icon: React.ReactNode;
}
const FEATURES: Feature[] = [
{
color: "emerald",
label: "Hand-Harvested",
headline: "Picked by Hand, Every Ear",
story: "Three generations of careful hands move through every row. No machine knows when an ear is perfectly ripe the way a farmer's daughter does.",
accentColor: "bg-emerald-500",
accentGlow: "hover:shadow-emerald-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-emerald-500 group-hover:to-emerald-600",
size: "tall",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 14c0-1 .9-2 2-2s2 1 2 2-.9 2-2 2-2-1-2-2z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 20v6c0 3 3 5 6 5s6-2 6-5v-6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M16 20v2M20 18v4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M28 14c0-1 .9-2 2-2s2 1 2 2-.9 2-2 2-2-1-2-2z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M28 20v6c0 3 3 5 6 5s6-2 6-5v-6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M32 20v2M36 18v4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 31v6h12v-6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M22 31h4M32 31h4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 14l4-4 4 4" />
</svg>
),
},
{
color: "emerald",
label: "Non-GMO",
headline: "Seed to Field, Naturally",
story: "Our Olathe Sweet seed has never been touched by a laboratory. The flavor was shaped by forty summers of patient selection — nothing added, nothing altered.",
accentColor: "bg-emerald-500",
accentGlow: "hover:shadow-emerald-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-emerald-500 group-hover:to-emerald-600",
size: "normal",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<circle cx="24" cy="24" r="10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 14v10l6 4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 10l-3-3 3-3 3 3-3 3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M30 10l3-3-3-3-3 3 3 3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 38l-3 3 3 3 3-3-3-3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M30 38l3 3 3-3-3-3 3-3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M10 24l-3 3 3 3 3-3-3-3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M38 24l3 3 3-3-3-3 3-3" />
<path strokeLinecap="round" strokeLinejoin="round" d="M14 18l2 2M32 18l2 2M14 30l2-2M32 30l2-2" />
</svg>
),
},
{
color: "emerald",
label: "Colorado Grown",
headline: "Born in the High Country",
story: "Olathe Sweet was developed for Colorado's mountain climate — intense sun by day, crisp nights by morning. That contrast makes a sweetness no other soil can claim.",
accentColor: "bg-emerald-500",
accentGlow: "hover:shadow-emerald-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-emerald-500 group-hover:to-emerald-600",
size: "normal",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M24 8l12 20H12L24 8z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 8l-6 10h12l-6-10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 28l6 12 6-12" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 40v-4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M6 40h36" />
</svg>
),
},
{
color: "emerald",
label: "Regenerative",
headline: "Our Roots Run Deep",
story: "We give back more than we take. Our soil is healthier today than forty years ago — not because we had to, but because this land deserves that kind of care.",
accentColor: "bg-emerald-500",
accentGlow: "hover:shadow-emerald-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-emerald-500 group-hover:to-emerald-600",
size: "tall",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M24 40v-16" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 24c0-8 6-14 14-14-2 8-8 14-14 14" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 24c0-8-6-14-14-14 2 8 8 14 14 14" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 28c0 6 5 10 10 10-2-6-6-10-10-10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 28c0 6-5 10-10 10 2-6 6-10 10-10" />
</svg>
),
},
{
color: "amber",
label: "Peak Freshness",
headline: "Harvested the Morning You Order",
story: "We pick before the heat steals a single calorie of sweetness. By the time your order arrives, the corn was still in the field that same morning.",
accentColor: "bg-amber-500",
accentGlow: "hover:shadow-amber-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-amber-500 group-hover:to-amber-600",
size: "normal",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<circle cx="24" cy="24" r="10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M24 14v10l6 4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 12l-2-2 2-2 2 2-2 2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M30 12l2-2-2-2-2 2 2 2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 36l-2 2 2 2 2-2-2-2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M30 36l2 2-2 2-2-2 2-2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 18l-2-2-2 2 2 2 2-2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 30l-2 2-2-2 2-2 2 2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M36 18l2-2 2 2-2 2-2-2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M36 30l2 2 2-2-2-2-2 2" />
</svg>
),
},
{
color: "amber",
label: "Grown in CO",
headline: "Your Neighborhood Farm",
story: "Forty years of Colorado stops. We know every town we serve — when they like to pick up, where they gather. Your order isn't a transaction; it's a relationship.",
accentColor: "bg-amber-500",
accentGlow: "hover:shadow-amber-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-amber-500 group-hover:to-amber-600",
size: "tall",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M8 30h32M8 30l6-10h20l6 10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M8 30v6h32v-6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M16 30v6M24 30v6M32 30v6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M16 26h16" />
<path strokeLinecap="round" strokeLinejoin="round" d="M4 36h8M36 36h8" />
</svg>
),
},
{
color: "amber",
label: "No Middleman",
headline: "From Our Rows to Your Table",
story: "No distribution center. No supermarket back shelf. Just the field, the cooler, and your front door — the same day. That's the promise we keep.",
accentColor: "bg-amber-500",
accentGlow: "hover:shadow-amber-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-amber-500 group-hover:to-amber-600",
size: "normal",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<rect x="8" y="14" width="32" height="24" rx="2" />
<path strokeLinecap="round" strokeLinejoin="round" d="M8 22h32" />
<path strokeLinecap="round" strokeLinejoin="round" d="M16 28h16M16 32h10" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 14v-4a6 6 0 0112 0v4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M18 10h12" />
<path strokeLinecap="round" strokeLinejoin="round" d="M4 38l4 4 4-4 4 4 4-4 4 4 4-4 4 4 4-4 4 4" />
</svg>
),
},
{
color: "amber",
label: "Easy Preorder",
headline: "Reserve in 30 Seconds",
story: "Choose your stop, tap your order, done. We'll have your corn in a labeled cooler with your name on it — no account, no app, no confusion.",
accentColor: "bg-amber-500",
accentGlow: "hover:shadow-amber-500/20",
accentHover: "group-hover:bg-gradient-to-r group-hover:from-amber-500 group-hover:to-amber-600",
size: "tall",
icon: (
<svg viewBox="0 0 48 48" fill="none" className="w-14 h-14 -mt-1" stroke="currentColor" strokeWidth="1.2">
<path strokeLinecap="round" strokeLinejoin="round" d="M16 16h16M16 22h16M16 28h12" />
<circle cx="36" cy="28" r="6" />
<path strokeLinecap="round" strokeLinejoin="round" d="M34 28l2 2 4-4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 12V8a4 4 0 014-4h16a4 4 0 014 4v4" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 12h24" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 12h4v10h-4" />
</svg>
),
},
];
// Single feature card with scroll-triggered entrance and hover effects
function FeatureCard({ feature, index }: { feature: Feature; index: number }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: "-60px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 48 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 48 }}
transition={{
duration: 0.7,
delay: (index % 4) * 0.1 + Math.floor(index / 4) * 0.08,
ease: [0.22, 0.61, 0.36, 1],
}}
className={`relative flex flex-col gap-5 rounded-2xl bg-stone-900/40 p-7 overflow-hidden group transition-all duration-500 hover:bg-stone-900/80 hover:translate-y-[-4px] hover:shadow-2xl ${feature.accentGlow} ${feature.accentHover}`}
>
{/* Animated top accent line */}
<div className="absolute top-0 left-7 right-7 h-px overflow-hidden">
<motion.div
className={`h-full ${feature.accentColor}`}
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.4, ease: "easeOut" }}
style={{ originX: 0 }}
/>
</div>
{/* Icon with glow on hover */}
<div className={`relative w-16 h-16 rounded-2xl flex items-center justify-center transition-all duration-500 ${
feature.color === "emerald"
? "bg-emerald-900/30 text-emerald-400 group-hover:bg-emerald-900/50 group-hover:text-emerald-300 group-hover:shadow-[0_0_28px_rgba(52,211,153,0.35)]"
: "bg-amber-900/30 text-amber-400 group-hover:bg-amber-900/50 group-hover:text-amber-300 group-hover:shadow-[0_0_28px_rgba(251,191,36,0.35)]"
}`}>
{feature.icon}
</div>
{/* Label */}
<p className={`hidden sm:block text-[10px] font-bold uppercase tracking-[0.2em] transition-colors duration-300 ${
feature.color === "emerald"
? "text-emerald-600/60 group-hover:text-emerald-400/80"
: "text-amber-600/60 group-hover:text-amber-400/80"
}`}>
{feature.label}
</p>
{/* Headline */}
<h3 className="text-white font-bold text-[15px] leading-snug tracking-tight">
{feature.headline}
</h3>
{/* Story */}
<p className="text-stone-500 text-[12px] leading-relaxed mt-auto">
{feature.story}
</p>
{/* Corner decoration */}
<div className={`absolute bottom-0 right-0 w-24 h-24 rounded-full blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 ${
feature.color === "emerald" ? "bg-emerald-600/10" : "bg-amber-600/10"
}`} />
</motion.div>
);
}
function WhyTuxedoCorn() {
const headerRef = useRef<HTMLDivElement>(null);
const headerInView = useInView(headerRef, { once: true, margin: "-80px" });
return (
<section className="relative bg-stone-950 py-32 overflow-hidden">
{/* Atmospheric glow */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_80%_30%,rgba(16,100,50,0.15)_0%,transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_20%_80%,rgba(120,75,20,0.12)_0%,transparent_50%)]" />
<LayoutContainer>
{/* Header */}
<div ref={headerRef} className="text-center mb-20 max-w-xl mx-auto">
<motion.p
initial={{ opacity: 0, y: 12 }}
animate={headerInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 12 }}
transition={{ duration: 0.6 }}
className="text-[11px] font-bold uppercase tracking-[0.3em] text-emerald-600/70 mb-6"
>
Why Olathe Sweet&trade;
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 16 }}
animate={headerInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 16 }}
transition={{ duration: 0.7, delay: 0.1 }}
className="text-5xl md:text-6xl font-black tracking-tight text-white leading-[1.05] mb-6"
>
Why Choose<br />Tuxedo Corn
</motion.h2>
<motion.div
initial={{ scaleX: 0 }}
animate={headerInView ? { scaleX: 1 } : { scaleX: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="mx-auto mt-1 mb-7 h-px w-16 bg-emerald-600/60"
style={{ originX: 0 }}
/>
<motion.p
initial={{ opacity: 0 }}
animate={headerInView ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="text-stone-400 text-base leading-relaxed"
>
Colorado has trusted Olathe Sweet at their tables for over four decades. Here is why.
</motion.p>
</div>
{/* Asymmetric masonry grid with staggered offsets */}
<div className="grid grid-cols-2 gap-5 lg:grid-cols-4">
{/* Row 1: tall, normal, tall, normal — with vertical offset stagger */}
<div>
<FeatureCard feature={FEATURES[0]} index={0} />
</div>
<div className="lg:mt-10">
<FeatureCard feature={FEATURES[1]} index={1} />
</div>
<div>
<FeatureCard feature={FEATURES[2]} index={2} />
</div>
<div className="lg:mt-10">
<FeatureCard feature={FEATURES[3]} index={3} />
</div>
{/* Row 2: normal, tall, normal, tall — offset in opposite direction */}
<div className="lg:mt-[-60px] hidden lg:block">
<FeatureCard feature={FEATURES[4]} index={4} />
</div>
<div>
<FeatureCard feature={FEATURES[5]} index={5} />
</div>
<div className="lg:mt-[-60px] hidden lg:block">
<FeatureCard feature={FEATURES[6]} index={6} />
</div>
<div>
<FeatureCard feature={FEATURES[7]} index={7} />
</div>
</div>
{/* Footer */}
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center text-[10px] font-medium uppercase tracking-[0.25em] text-stone-700 mt-16"
>
Olathe Sweet&trade; &nbsp;·&nbsp; Olathe, Colorado
</motion.p>
</LayoutContainer>
</section>
);
}
// ── Section header ──────────────────────────────────────────────────
function SectionHeader({
eyebrow,
headline,
subtext,
accent = "emerald",
}: {
eyebrow: string;
headline: string;
subtext: string;
accent?: "emerald" | "stone";
}) {
return (
<div className="mb-14">
<p className={`text-[11px] font-semibold uppercase tracking-widest ${accent === "emerald" ? "text-emerald-600" : "text-stone-500"} mb-4`}>
{eyebrow}
</p>
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
{headline}
</h2>
<div className="mt-6 h-px w-12 bg-emerald-600" />
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
{subtext}
</p>
</div>
);
}
export default function TuxedoPage() {
const [brand, setBrand] = useState<Brand | null>(null);
const [stops, setStops] = useState<Stop[]>([]);
const [products, setProducts] = useState<Product[]>([]);
const [wholesaleEnabled, setWholesaleEnabled] = useState(false);
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
const [olatheSweetLogoUrl, setOlatheSweetLogoUrl] = useState<string | null>(null);
const [olatheSweetLogoUrlDark, setOlatheSweetLogoUrlDark] = useState<string | null>(null);
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
const [heroTagline, setHeroTagline] = useState<string | null>(null);
const [heroImageUrl, setHeroImageUrl] = useState<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
const [contactEmail, setContactEmail] = useState<string | null>(null);
const [contactPhone, setContactPhone] = useState<string | null>(null);
const [brandPrimaryColor, setBrandPrimaryColor] = useState<string | null>(null);
const [brandBgColor, setBrandBgColor] = useState<string | null>(null);
const [brandTextColor, setBrandTextColor] = useState<string | null>(null);
useEffect(() => {
async function load() {
const slug = "tuxedo";
const [brandResult, settingsResult] = await Promise.all([
supabase.from("brands").select("*").eq("slug", slug).single(),
getBrandSettingsPublic(slug),
]);
const brandData = brandResult.data;
setBrand(brandData);
if (settingsResult.success && settingsResult.settings) {
const s = settingsResult.settings;
setLogoUrl(s.logo_url ?? null);
setLogoUrlDark(s.logo_url_dark ?? null);
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null);
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? null);
setHeroImageUrl(s.hero_image_url ?? null);
setHeroTagline(s.hero_tagline ?? null);
setCustomFooterText(s.custom_footer_text ?? null);
setContactEmail(s.email ?? null);
setContactPhone(s.phone ?? null);
setShowSchedulePdf(s.show_schedule_pdf ?? true);
setShowWholesaleLink(s.show_wholesale_link ?? true);
setWholesaleEnabled(settingsResult.wholesaleEnabled ?? false);
setBrandPrimaryColor(s.brand_primary_color ?? null);
setBrandBgColor(s.brand_bg_color ?? null);
setBrandTextColor(s.brand_text_color ?? null);
}
try {
const { getCurrentAdminUser } = await import("@/actions/admin-user");
const adminUser = await getCurrentAdminUser();
setIsAdmin(!!adminUser);
} catch {
// not logged in as admin
}
if (brandData?.id) {
const [{ data: stopsData }, { data: productsData }] = await Promise.all([
supabase.from("stops").select("*").eq("brand_id", brandData.id).eq("active", true),
supabase.from("products").select("*").eq("brand_id", brandData.id).eq("active", true),
]);
setStops(stopsData ?? []);
setProducts(productsData ?? []);
}
}
load();
}, []);
function scrollToStops() {
document.getElementById("stops")?.scrollIntoView({ behavior: "smooth" });
}
function scrollToStory() {
document.getElementById("story")?.scrollIntoView({ behavior: "smooth" });
}
function scrollToProducts() {
document.getElementById("products")?.scrollIntoView({ behavior: "smooth" });
}
const featuredProducts = products.slice(0, 3);
return (
<div className="min-h-screen bg-stone-50">
<BrandStylesProvider
primaryColor={brandPrimaryColor}
bgColor={brandBgColor}
textColor={brandTextColor}
/>
<StorefrontHeader
brandName={brand?.name ?? "Tuxedo Corn"}
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
showWholesaleLink={showWholesaleLink}
isAdmin={isAdmin}
brandAccent="green"
/>
<main>
{showWholesaleLink && wholesaleEnabled && (
<div className="bg-stone-950 border-b border-zinc-800 py-3.5">
<div className="mx-auto max-w-6xl px-6">
<Link
href="/wholesale/login"
className="inline-flex items-center gap-2 rounded-full bg-emerald-700 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-600 transition-colors"
>
Wholesale Portal
</Link>
</div>
</div>
)}
<TuxedoVideoHero
eyebrow="Olathe Sweet™ — Olathe, Colorado"
title="Tuxedo Corn"
description={
heroTagline ??
"Premium Olathe Sweet™ sweet corn — hand-picked at peak freshness from our family farm in Colorado."
}
olatheSweetLogoUrl={olatheSweetLogoUrl}
primaryButton="Find a Stop"
secondaryButton="Our Story"
onPrimaryClick={scrollToStops}
onSecondaryClick={scrollToStory}
/>
<WhyTuxedoCorn />
<section id="stops" className="py-28 bg-gradient-to-b from-stone-100 to-stone-50 scroll-mt-20">
<LayoutContainer>
<div className="mb-14 flex items-end justify-between">
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
Upcoming<br className="hidden md:block" /> Stops
</h2>
<div className="mt-6 h-px w-12 bg-emerald-600" />
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
</p>
</div>
{showSchedulePdf && (
<a
href="/api/tuxedo/schedule-pdf"
download
className="shrink-0 hidden md:inline-flex items-center gap-2.5 rounded-2xl bg-stone-900 px-6 py-3.5 text-sm font-semibold text-white hover:bg-stone-800 active:bg-stone-950 transition-colors"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Download Schedule
</a>
)}
</div>
{stops.length === 0 ? (
<div className="rounded-3xl bg-white p-14 text-center ring-1 ring-stone-200/60">
<p className="text-stone-500">No upcoming stops scheduled. Check back soon!</p>
</div>
) : (
<PaginatedStops stops={stops} brandSlug="tuxedo" brandAccent="green" />
)}
</LayoutContainer>
</section>
{/* Section divider */}
<div className="relative h-24 bg-stone-50 overflow-hidden">
<div className="absolute inset-0 flex items-center justify-center">
<div className="flex items-center gap-6">
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
<svg className="h-6 w-6 text-stone-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v13m0 0V8m0 13V4m0 9H4m8 0h8m-8-4h8" />
</svg>
<div className="h-px w-24 bg-gradient-to-r from-transparent via-stone-300 to-transparent" />
</div>
</div>
</div>
<section id="products" className="py-28 bg-white">
<LayoutContainer>
<div className="mb-14">
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
This Week's<br className="hidden md:block" /> Harvest
</h2>
<div className="mt-6 h-px w-12 bg-emerald-600" />
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
</p>
</div>
{featuredProducts.length === 0 ? (
<div className="rounded-3xl bg-stone-50 p-14 text-center ring-1 ring-stone-200/60">
<p className="text-stone-500">No products available at this time.</p>
</div>
) : (
<div className="grid gap-8 md:grid-cols-3">
{featuredProducts.map((product) => (
<ProductCard
key={product.id}
id={product.id}
name={product.name}
description={product.description}
price={`$${product.price}`}
type={product.type}
imageUrl={product.image_url}
brandSlug="tuxedo"
brandName="Tuxedo Corn"
brandId={product.brand_id}
brandAccent="green"
is_taxable={product.is_taxable}
pickup_type={product.pickup_type}
olatheSweetLogoUrlDark={olatheSweetLogoUrlDark}
/>
))}
</div>
)}
{products.length > 3 && (
<div className="mt-10 text-center">
<p className="text-sm text-stone-400">{products.length - 3} more products available in the full catalog.</p>
</div>
)}
</LayoutContainer>
</section>
<section id="story" className="py-24 bg-stone-950">
<LayoutContainer>
<div className="text-center max-w-2xl mx-auto">
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-white leading-[1.05] mb-6">
Three Generations of Sweet Corn Excellence
</h2>
<div className="mx-auto mt-6 mb-8 h-px w-12 bg-emerald-600/50" />
<p className="text-stone-400 leading-relaxed text-lg">
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
</p>
<div className="flex items-center justify-center gap-12 mt-10">
{[
{ stat: "40+", label: "Years" },
{ stat: "3", label: "Generations" },
{ stat: "100%", label: "Hand-Picked" },
].map(({ stat, label }) => (
<div key={label} className="text-center">
<p className="text-4xl font-black text-white">{stat}</p>
<p className="mt-1 text-xs text-stone-500 uppercase tracking-widest">{label}</p>
</div>
))}
</div>
<Link
href="/tuxedo/about"
className="inline-flex items-center gap-2.5 mt-12 rounded-full bg-emerald-700 px-8 py-4 text-sm font-bold text-white hover:bg-emerald-600 active:bg-emerald-800 transition-colors"
>
Read Our Story
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</Link>
</div>
</LayoutContainer>
</section>
</main>
<StorefrontFooter
brandName={brand?.name ?? "Tuxedo Corn"}
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
customFooterText={customFooterText}
contactEmail={contactEmail}
contactPhone={contactPhone}
isAdmin={isAdmin}
brandAccent="green"
/>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
export default function Loading() {
return (
<main className="min-h-screen bg-yellow-50 px-6 py-12">
<div className="mx-auto max-w-5xl">
<div className="animate-pulse space-y-6">
<div className="h-6 w-32 rounded bg-slate-200" />
<div className="h-12 w-80 rounded bg-slate-200" />
<div className="mt-8 rounded-2xl bg-white p-8 shadow-sm" />
<div className="mt-10 grid gap-6 md:grid-cols-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-64 rounded-2xl bg-slate-200" />
))}
</div>
</div>
</div>
</main>
);
}
+225
View File
@@ -0,0 +1,225 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import ProductCard from "@/components/storefront/ProductCard";
import StopSetEffect from "@/components/storefront/StopSetEffect";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
import { supabase } from "@/lib/supabase";
import { formatDate } from "@/lib/format-date";
type Product = {
id: string;
name: string;
description: string | null;
price: number;
type: string;
image_url: string | null;
brand_id: string;
is_taxable?: boolean;
pickup_type?: "scheduled_stop" | "shed";
};
export default function StopPage() {
const params = useParams();
const slug = params.slug as string;
const [stop, setStop] = useState<{
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
brand_id: string;
} | null>(null);
const [products, setProducts] = useState<Product[]>([]);
const [brandSlug, setBrandSlug] = useState("tuxedo");
const [brandAccent, setBrandAccent] = useState<"green" | "orange" | "blue">("green");
useEffect(() => {
async function load() {
const { data: stopData } = await supabase
.from("stops")
.select("*")
.eq("slug", slug)
.eq("active", true)
.single();
if (!stopData) return;
setStop(stopData);
const isIndian = slug.includes("indian");
setBrandSlug(isIndian ? "indian-river-direct" : "tuxedo");
setBrandAccent(isIndian ? "blue" : "green");
const { data: productsData } = await supabase
.from("products")
.select("*")
.eq("brand_id", stopData.brand_id)
.eq("active", true);
setProducts(productsData ?? []);
}
load();
}, [slug]);
if (!stop) {
return (
<div className="min-h-screen bg-stone-50">
<StorefrontHeader brandName="Tuxedo Corn" brandSlug="tuxedo" brandAccent="green" />
<main className="py-20">
<LayoutContainer>
<div className="max-w-5xl mx-auto text-center">
<h1 className="text-5xl font-black tracking-tight text-stone-950">Stop Not Found</h1>
</div>
</LayoutContainer>
</main>
</div>
);
}
const isBlue = brandAccent === "blue";
const brandLabel = isBlue ? "Indian River Direct" : "Tuxedo Corn";
return (
<div className="min-h-screen bg-stone-50">
<StorefrontHeader
brandName={brandLabel}
brandSlug={brandSlug}
brandAccent={brandAccent}
/>
<main className="py-16 md:py-20">
<LayoutContainer>
<div className="max-w-5xl mx-auto">
{/* Back navigation */}
<div className="mb-10 flex items-center gap-2">
<Link
href={`/${brandSlug}#stops`}
className="flex items-center gap-2 text-sm font-medium text-stone-500 hover:text-stone-800 transition-colors group"
>
<svg
className="h-4 w-4 transition-transform duration-200 group-hover:-translate-x-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg>
All Stops
</Link>
<span className="text-stone-300">/</span>
<span className="text-sm text-stone-700 font-medium">{stop.city}, {stop.state}</span>
</div>
{/* Stop header */}
<div className="mb-12">
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">
{brandLabel}
</p>
<h1 className="text-5xl md:text-7xl font-black tracking-tight text-stone-950 leading-[1.0]">
{stop.city},<br className="hidden md:block" /> {stop.state}
</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&trade; sweet corn for pickup at this stop."}
</p>
<div className="mt-6 h-px w-12 bg-emerald-600" />
</div>
{/* Stop info */}
<div className="rounded-3xl bg-white p-8 mb-14 shadow-sm ring-1 ring-stone-200/60">
<div className="grid gap-4 md:grid-cols-3">
<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"}`}>
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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" />
</svg>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Date</p>
<p className="font-bold text-stone-950">{formatDate(stop.date)}</p>
</div>
</div>
<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"}`}>
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Time</p>
<p className="font-bold text-stone-950">{stop.time}</p>
</div>
</div>
<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"}`}>
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<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>
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Location</p>
<p className="font-bold text-stone-950 leading-tight">{stop.location}</p>
</div>
</div>
</div>
</div>
{/* 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 ? (
<div className="rounded-3xl bg-white p-12 text-center shadow-sm ring-1 ring-stone-200/60">
<p className="text-stone-500">No products available for this stop.</p>
</div>
) : (
<div className="grid gap-8 md:grid-cols-3">
{products.map((product) => (
<ProductCard
key={product.id}
id={product.id}
name={product.name}
description={product.description}
price={`$${product.price}`}
type={product.type}
imageUrl={product.image_url}
brandSlug={brandSlug}
brandName={brandLabel}
brandId={product.brand_id}
brandAccent={brandAccent}
is_taxable={product.is_taxable}
pickup_type={product.pickup_type}
/>
))}
</div>
)}
</section>
</div>
</LayoutContainer>
</main>
<StorefrontFooter
brandName={brandLabel}
brandSlug={brandSlug}
brandAccent={brandAccent}
/>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import TimeTrackingFieldClient from "@/components/time-tracking/TimeTrackingFieldClient";
const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
const BRAND_NAME = "Tuxedo Corn";
const BRAND_ACCENT = "green";
export const metadata = {
title: "Worker Clock — Tuxedo Corn",
};
export default function TuxedoTimeClockPage() {
return (
<TimeTrackingFieldClient
brandId={BRAND_ID}
brandName={BRAND_NAME}
brandAccent={BRAND_ACCENT}
/>
);
}