Files
route-commerce/src/app/tuxedo/faq/FAQClientPage.tsx
T
tyler 778b3fe311 fix: full Apple HIG mobile + SEO audit - all issues resolved
MOBILE RESPONSIVENESS (Apple HIG):
- HeroSection: typography scaled 7xl→4xl sm:5xl md:6xl lg:7xl, responsive px/py, rounded-2xl, active:scale-95
- Cart quantity buttons: h-8→h-11 w-8→w-11 (44px touch target), rounded-xl, active:scale-95
- StorefrontFooter newsletter: responsive w-full sm:w-52 lg:w-64, aria-label, larger touch targets
- StorefrontHeader mobile nav: padding px-6→px-4 py-6→py-5
- AdminTable: overflow-x-auto + min-w-[600px] for horizontal scroll
- AdminOrdersPanel: same table overflow fix
- AdminLayout: mobile px-4 sm:px-6 py-6 sm:py-10
- AdminFilterTabs: responsive text/text sizes
- AdminSidebar hamburger: h-10→h-11 w-10→w-11 (44px touch target)
- DashboardClient: grid gap-3 sm:gap-4, responsive stat text
- OrderEditForm: grid-cols-1 sm:grid-cols-2 (was 2, breaks on mobile)
- BillingClient: min-h-[44px] on select/button
- ProductsClient: h-32 sm:h-40 responsive image height
- StopCard: line-clamp-2 instead of line-clamp-1 on location
- CommunicationsPage: tabs wrapped in overflow-x-auto
- Checkout page: grid breakpoint md not lg
- Tuxedo page: SectionHeader mobile-first, feature grid grid-cols-1 sm:2, label visible
- Tuxedo stats: text-3xl sm:text-4xl

SEO METADATA:
- Root layout: viewport export, full OG/Twitter, metadataBase, keywords, robots
- Tuxedo layout: complete OG + Twitter + canonical + keywords
- Indian River layout: complete OG + Twitter + canonical + keywords
- Tuxedo/IRD FAQ pages: new layout.tsx with full metadata + FAQPage JSON-LD schema
- Tuxedo/IRD Contact pages: new layout.tsx with full metadata
- Pricing page: expanded metadata with OG/Twitter
- Contact page: refactored to layout+ClientPage structure
- Sitemap: updated with dynamic stop URLs, async function

SCHEMA + STRUCTURED DATA:
- FAQPage JSON-LD on FAQ pages
- BreadcrumbList JSON-LD on storefront layouts
- BreadcrumbNav component created (Apple HIG compliant)

BUG FIXES:
- Indian River: replaced raw <img> with Next.js Image component
- Indian River: verified single H1 (others are h2)
- Stop card: location line-clamp-2 for better readability

TYPE CHECK: all pass
2026-06-02 04:32:58 +00:00

234 lines
10 KiB
TypeScript

"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>
);
}