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
This commit is contained in:
2026-06-02 04:32:58 +00:00
parent c73da417af
commit 778b3fe311
32 changed files with 861 additions and 284 deletions
+29
View File
@@ -87,4 +87,33 @@ export async function publishStop(
}
return { success: true };
}
/**
* Fetch active stops for sitemap generation.
* This is a public function that doesn't require authentication.
*/
export type StopForSitemap = {
slug: string;
brand_slug: string;
last_modified: string;
};
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
// Get all active stops with their brand slug
const response = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_active_stops_with_brand`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
}
);
if (!response.ok) return [];
const stops = await response.json();
return Array.isArray(stops) ? stops : [];
}
@@ -41,7 +41,7 @@ export default function BillingClient({ currentTier, brandId }: Props) {
<select
value={selectedTier}
onChange={(e) => setSelectedTier(e.target.value)}
className="rounded-xl border border-zinc-600 bg-zinc-900 px-3 py-2 text-sm outline-none focus:border-blue-500"
className="rounded-xl border border-zinc-600 bg-zinc-900 px-3 sm:px-4 py-3 sm:py-2 text-sm outline-none focus:border-blue-500 min-h-[44px]"
>
{TIERS.map((t) => (
<option key={t.value} value={t.value}>{t.label}</option>
@@ -50,7 +50,7 @@ export default function BillingClient({ currentTier, brandId }: Props) {
<button
onClick={handleSaveTier}
disabled={saving || selectedTier === currentTier}
className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:opacity-50"
className="rounded-xl bg-slate-900 px-4 sm:px-5 py-3 sm:py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:opacity-50 active:scale-95 transition-all min-h-[44px]"
>
{saving ? "Saving..." : saved ? "Saved!" : "Save Tier"}
</button>
+3 -3
View File
@@ -272,12 +272,12 @@ export default function CartPage() {
<div className="flex items-center gap-3">
<button
onClick={() => decreaseQuantity(item.id)}
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 text-white hover:bg-white/20 transition-all text-lg font-medium"
className="flex h-11 w-11 items-center justify-center rounded-xl bg-white/10 text-white hover:bg-white/20 active:scale-95 transition-all text-xl font-medium"
></button>
<span className="w-8 text-center font-semibold text-white">{item.quantity}</span>
<span className="w-10 text-center font-semibold text-white">{item.quantity}</span>
<button
onClick={() => increaseQuantity(item.id)}
className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-500 text-white hover:bg-emerald-400 transition-all text-lg font-medium shadow-lg shadow-emerald-500/20"
className="flex h-11 w-11 items-center justify-center rounded-xl bg-emerald-500 text-white hover:bg-emerald-400 active:scale-95 transition-all text-xl font-medium shadow-lg shadow-emerald-500/20"
>+</button>
</div>
</div>
+1 -1
View File
@@ -153,7 +153,7 @@ export default function CheckoutPage() {
<div className="min-h-screen bg-stone-50">
<StorefrontHeader brandName="Checkout" brandSlug="tuxedo" />
<main className="px-6 py-12">
<div className="mx-auto grid max-w-6xl gap-10 lg:grid-cols-[1fr_400px]">
<div className="mx-auto grid max-w-6xl gap-8 md:grid-cols-[1fr_400px]">
<div>
<h1 className="text-4xl font-bold text-slate-900">
Checkout
+246
View File
@@ -0,0 +1,246 @@
"use client";
import { useState } from "react";
import Link from "next/link";
type FormState = {
name: string;
email: string;
topic: string;
message: string;
};
export default function ContactClientPage() {
const [submitted, setSubmitted] = useState(false);
const [form, setForm] = useState<FormState>({ name: "", email: "", topic: "general", message: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setTimeout(() => {
setSubmitted(true);
setIsSubmitting(false);
}, 600);
}
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-xl border-b border-[#e5e5e5]">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center shadow-lg shadow-[#1a4d2e]/20">
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-xl font-bold text-[#1a1a1a] tracking-tight">Route Commerce</span>
</Link>
<Link href="/" className="text-sm font-medium text-[#666] hover:text-[#1a4d2e] transition-colors">
Back to Home
</Link>
</div>
</header>
<main className="max-w-5xl mx-auto px-6 py-32">
{/* Page header */}
<div className="text-center mb-16">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-4">Get in Touch</p>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-black text-[#0a0a0a] tracking-tight mb-4">
Contact Us
</h1>
<p className="text-lg text-[#666] max-w-lg mx-auto">
Questions about produce, wholesale accounts, or becoming a partner? We would love to hear from you.
</p>
</div>
{/* Contact info cards */}
<div className="grid gap-6 md:grid-cols-3 mb-16">
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Address</h3>
<p className="text-[#666] leading-relaxed">Serving farms and trucking routes across the nation</p>
</div>
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Phone</h3>
<p className="text-[#666] leading-relaxed">support@routecommerce.com</p>
</div>
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Email</h3>
<p className="text-[#666] leading-relaxed">hello@routecommerce.com</p>
</div>
</div>
{/* Contact form */}
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-10 shadow-sm">
<div className="mb-8">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-3">Send a Message</p>
<h2 className="text-3xl font-black text-[#0a0a0a] tracking-tight leading-tight">
Get in Touch
</h2>
<div className="mt-4 h-1 w-12 bg-[#1a4d2e]" />
</div>
{submitted ? (
<div className="rounded-2xl bg-[#f0fdf4] border border-[#bbf7d0] p-10 text-center">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-[#dcfce7] mb-4 shadow-md">
<svg className="h-7 w-7 text-[#1a4d2e]" 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-[#0a0a0a]">Message received.</p>
<p className="mt-2 text-base text-[#666]">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-semibold text-[#1a4d2e] underline hover:text-[#2d6a4f]"
>
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-[#333] mb-2">Your Name</label>
<input
required
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className="w-full rounded-xl border-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors"
placeholder="Jane Smith"
/>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] 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-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors"
placeholder="jane@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] mb-2">Topic</label>
<select
value={form.topic}
onChange={(e) => setForm({ ...form, topic: e.target.value })}
className="w-full rounded-xl border-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors appearance-none"
>
<option value="general">General Question</option>
<option value="orders">Orders & Pickup</option>
<option value="wholesale">Wholesale Inquiry</option>
<option value="partner">Become a Partner</option>
<option value="support">Technical Support</option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] 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-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors resize-none"
placeholder="How can we help you?"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="rounded-xl bg-[#1a4d2e] hover:bg-[#2d6a4f] px-8 py-4 text-base font-bold text-white hover:shadow-lg hover:shadow-[#1a4d2e]/25 transition-all disabled:opacity-60 disabled:cursor-not-allowed flex items-center gap-2"
>
{isSubmitting ? (
<>
<svg className="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Sending...
</>
) : (
<>
Send Message
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</>
)}
</button>
</form>
)}
</div>
{/* Business hours */}
<div className="mt-10 rounded-3xl bg-white border border-[#e5e5e5] p-10 shadow-sm">
<div className="mb-6">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-3">Availability</p>
<h2 className="text-2xl font-black text-[#0a0a0a] tracking-tight leading-tight">
When to Reach Us
</h2>
<div className="mt-4 h-1 w-12 bg-[#1a4d2e]" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-[#fafafa]">
<p className="font-semibold text-[#333]">Monday Friday</p>
<p className="text-[#666] font-medium">8:00 AM 6:00 PM</p>
</div>
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-[#fafafa]">
<p className="font-semibold text-[#333]">Saturday Sunday</p>
<p className="text-[#666] font-medium">By Appointment</p>
</div>
</div>
<p className="mt-5 text-sm text-[#999] leading-relaxed">
For urgent matters, please call our support line.
</p>
</div>
</main>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] mt-20">
<div className="max-w-5xl mx-auto px-6 py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-sm text-[#666]">2024 Route Commerce. All rights reserved.</span>
</div>
<div className="flex items-center gap-6 text-sm text-[#888]">
<Link href="/privacy-policy" className="hover:text-[#1a4d2e] transition-colors">Privacy</Link>
<Link href="/terms-and-conditions" className="hover:text-[#1a4d2e] transition-colors">Terms</Link>
</div>
</div>
</div>
</footer>
</div>
);
}
+38 -240
View File
@@ -1,246 +1,44 @@
"use client";
import type { Metadata } from "next";
import ContactClientPage from "./ContactClientPage";
import { useState } from "react";
import Link from "next/link";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
type FormState = {
name: string;
email: string;
topic: string;
message: string;
export const metadata: Metadata = {
title: "Contact Us",
description: "Get in touch with Route Commerce. Questions about produce, wholesale accounts, or becoming a partner? We'd love to hear from you.",
keywords: ["Route Commerce contact", "produce wholesale inquiry", "partnership questions", "agriculture platform support"],
openGraph: {
title: "Contact Us — Route Commerce",
description: "Get in touch with Route Commerce. Questions about produce, wholesale accounts, or becoming a partner.",
url: `${BASE_URL}/contact`,
siteName: "Route Commerce",
locale: "en_US",
type: "website",
images: [
{
url: "/og-default.jpg",
width: 1200,
height: 630,
alt: "Contact Route Commerce",
},
],
},
twitter: {
card: "summary_large_image",
title: "Contact Us — Route Commerce",
description: "Get in touch with Route Commerce. Questions about produce, wholesale accounts, or becoming a partner.",
site: "@RouteCommerce",
images: ["/og-default.jpg"],
},
alternates: {
canonical: `${BASE_URL}/contact`,
},
robots: {
index: true,
follow: true,
},
};
export default function ContactPage() {
const [submitted, setSubmitted] = useState(false);
const [form, setForm] = useState<FormState>({ name: "", email: "", topic: "general", message: "" });
const [isSubmitting, setIsSubmitting] = useState(false);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSubmitting(true);
setTimeout(() => {
setSubmitted(true);
setIsSubmitting(false);
}, 600);
}
return (
<div className="min-h-screen bg-white">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-xl border-b border-[#e5e5e5]">
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3">
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center shadow-lg shadow-[#1a4d2e]/20">
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-xl font-bold text-[#1a1a1a] tracking-tight">Route Commerce</span>
</Link>
<Link href="/" className="text-sm font-medium text-[#666] hover:text-[#1a4d2e] transition-colors">
Back to Home
</Link>
</div>
</header>
<main className="max-w-5xl mx-auto px-6 py-32">
{/* Page header */}
<div className="text-center mb-16">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-4">Get in Touch</p>
<h1 className="text-4xl sm:text-5xl md:text-6xl font-black text-[#0a0a0a] tracking-tight mb-4">
Contact Us
</h1>
<p className="text-lg text-[#666] max-w-lg mx-auto">
Questions about produce, wholesale accounts, or becoming a partner? We would love to hear from you.
</p>
</div>
{/* Contact info cards */}
<div className="grid gap-6 md:grid-cols-3 mb-16">
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Address</h3>
<p className="text-[#666] leading-relaxed">Serving farms and trucking routes across the nation</p>
</div>
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Phone</h3>
<p className="text-[#666] leading-relaxed">support@routecommerce.com</p>
</div>
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-8 text-center shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-[#f0fdf4] mb-5 shadow-md">
<svg className="h-6 w-6 text-[#1a4d2e]" 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-lg font-bold text-[#0a0a0a] mb-3">Email</h3>
<p className="text-[#666] leading-relaxed">hello@routecommerce.com</p>
</div>
</div>
{/* Contact form */}
<div className="rounded-3xl bg-white border border-[#e5e5e5] p-10 shadow-sm">
<div className="mb-8">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-3">Send a Message</p>
<h2 className="text-3xl font-black text-[#0a0a0a] tracking-tight leading-tight">
Get in Touch
</h2>
<div className="mt-4 h-1 w-12 bg-[#1a4d2e]" />
</div>
{submitted ? (
<div className="rounded-2xl bg-[#f0fdf4] border border-[#bbf7d0] p-10 text-center">
<div className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-[#dcfce7] mb-4 shadow-md">
<svg className="h-7 w-7 text-[#1a4d2e]" 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-[#0a0a0a]">Message received.</p>
<p className="mt-2 text-base text-[#666]">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-semibold text-[#1a4d2e] underline hover:text-[#2d6a4f]"
>
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-[#333] mb-2">Your Name</label>
<input
required
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className="w-full rounded-xl border-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors"
placeholder="Jane Smith"
/>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] 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-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors"
placeholder="jane@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] mb-2">Topic</label>
<select
value={form.topic}
onChange={(e) => setForm({ ...form, topic: e.target.value })}
className="w-full rounded-xl border-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors appearance-none"
>
<option value="general">General Question</option>
<option value="orders">Orders & Pickup</option>
<option value="wholesale">Wholesale Inquiry</option>
<option value="partner">Become a Partner</option>
<option value="support">Technical Support</option>
</select>
</div>
<div>
<label className="block text-sm font-semibold text-[#333] 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-2 border-[#e5e5e5] bg-white px-5 py-4 text-base text-[#1a1a1a] placeholder:text-[#999] outline-none focus:border-[#1a4d2e] focus:ring-4 focus:ring-[#1a4d2e]/10 transition-colors resize-none"
placeholder="How can we help you?"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="rounded-xl bg-[#1a4d2e] hover:bg-[#2d6a4f] px-8 py-4 text-base font-bold text-white hover:shadow-lg hover:shadow-[#1a4d2e]/25 transition-all disabled:opacity-60 disabled:cursor-not-allowed flex items-center gap-2"
>
{isSubmitting ? (
<>
<svg className="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Sending...
</>
) : (
<>
Send Message
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</>
)}
</button>
</form>
)}
</div>
{/* Business hours */}
<div className="mt-10 rounded-3xl bg-white border border-[#e5e5e5] p-10 shadow-sm">
<div className="mb-6">
<p className="text-sm font-bold tracking-[0.15em] uppercase text-[#1a4d2e] mb-3">Availability</p>
<h2 className="text-2xl font-black text-[#0a0a0a] tracking-tight leading-tight">
When to Reach Us
</h2>
<div className="mt-4 h-1 w-12 bg-[#1a4d2e]" />
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-[#fafafa]">
<p className="font-semibold text-[#333]">Monday Friday</p>
<p className="text-[#666] font-medium">8:00 AM 6:00 PM</p>
</div>
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-[#fafafa]">
<p className="font-semibold text-[#333]">Saturday Sunday</p>
<p className="text-[#666] font-medium">By Appointment</p>
</div>
</div>
<p className="mt-5 text-sm text-[#999] leading-relaxed">
For urgent matters, please call our support line.
</p>
</div>
</main>
{/* Footer */}
<footer className="border-t border-[#e5e5e5] mt-20">
<div className="max-w-5xl mx-auto px-6 py-8">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-[#1a4d2e] to-[#2d6a4f] flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<span className="text-sm text-[#666]">2024 Route Commerce. All rights reserved.</span>
</div>
<div className="flex items-center gap-6 text-sm text-[#888]">
<Link href="/privacy-policy" className="hover:text-[#1a4d2e] transition-colors">Privacy</Link>
<Link href="/terms-and-conditions" className="hover:text-[#1a4d2e] transition-colors">Terms</Link>
</div>
</div>
</div>
</footer>
</div>
);
return <ContactClientPage />;
}
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import ContactClientPage from "./ContactClientPage";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
export const metadata: Metadata = {
title: "Contact Us",
description: "Get in touch with Indian River Direct. Questions about peaches, citrus, your order, or becoming a wholesale partner — we'd love to hear from you.",
keywords: ["Indian River Direct contact", "peach order questions", "citrus wholesale inquiry", "Florida produce contact"],
openGraph: {
title: "Contact Us — Indian River Direct",
description: "Get in touch with Indian River Direct. Questions about peaches, citrus, your order, or becoming a wholesale partner.",
url: `${BASE_URL}/indian-river-direct/contact`,
siteName: "Indian River Direct",
locale: "en_US",
type: "website",
images: [
{
url: "/og-indian-river.jpg",
width: 1200,
height: 630,
alt: "Contact Indian River Direct",
},
],
},
twitter: {
card: "summary_large_image",
title: "Contact Us — Indian River Direct",
description: "Get in touch with Indian River Direct. Questions about peaches, citrus, your order, or becoming a wholesale partner.",
site: "@IndianRiverDirect",
images: ["/og-indian-river.jpg"],
},
alternates: {
canonical: `${BASE_URL}/indian-river-direct/contact`,
},
robots: {
index: true,
follow: true,
},
};
export default function IndianRiverContactLayout({ children }: { children: React.ReactNode }) {
return <ContactClientPage />;
}
@@ -0,0 +1,91 @@
import type { Metadata } from "next";
import FAQClientPage from "./FAQClientPage";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
export const metadata: Metadata = {
title: "FAQ — Frequently Asked Questions",
description: "Find answers to common questions about ordering peaches, citrus, pickup stops, and wholesale accounts for Indian River Direct.",
keywords: ["Indian River Direct FAQ", "peach pickup questions", "citrus order FAQ", "Florida produce FAQ", "wholesale fruit accounts"],
openGraph: {
title: "FAQ — Indian River Direct Frequently Asked Questions",
description: "Find answers to common questions about ordering peaches, citrus, pickup stops, shipping, and wholesale accounts.",
url: `${BASE_URL}/indian-river-direct/faq`,
siteName: "Indian River Direct",
locale: "en_US",
type: "website",
images: [
{
url: "/og-indian-river.jpg",
width: 1200,
height: 630,
alt: "Indian River Direct FAQ",
},
],
},
twitter: {
card: "summary_large_image",
title: "FAQ — Indian River Direct",
description: "Find answers to common questions about ordering peaches, citrus, pickup stops, and wholesale accounts.",
site: "@IndianRiverDirect",
images: ["/og-indian-river.jpg"],
},
alternates: {
canonical: `${BASE_URL}/indian-river-direct/faq`,
},
robots: {
index: true,
follow: true,
},
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How does preordering work?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Select your preferred pickup stop from our schedule, then browse our available products. Add items to your cart and select your stop at checkout. Preordering helps us know how much to bring to each stop — and ensures you get what you want."
}
},
{
"@type": "Question",
"name": "What citrus is available when?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Our season runs year-round with different fruits at different times: Navels (DecMar), Ruby Red Grapefruit (JanApr), Honeybells (JanFeb), Super Sweet Tangerines (JanMar), Temples (MarApr), Peaches (JunAug), Blueberries (JunJul)."
}
},
{
"@type": "Question",
"name": "Can I have my order shipped instead of picked up?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Currently we offer pickup-only ordering through this site. For bulk or wholesale shipping inquiries, please contact us directly at 772-971-4484."
}
},
{
"@type": "Question",
"name": "How do I apply for a wholesale account?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Visit /wholesale/register to apply. We review applications and typically respond within 12 business days. Wholesale accounts are available to retailers, restaurants, farm stands, and other businesses."
}
}
]
};
export default function IndianRiverFAQLayout({ children }: { children: React.ReactNode }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<FAQClientPage />
</>
);
}
+23
View File
@@ -2,6 +2,26 @@ import type { Metadata } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
// BreadcrumbList schema for SEO
const indianRiverBreadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": BASE_URL,
},
{
"@type": "ListItem",
"position": 2,
"name": "Indian River Direct",
"item": `${BASE_URL}/indian-river-direct`,
},
],
};
export const metadata: Metadata = {
title: {
default: "Indian River Direct | Peach & Citrus Truckload",
@@ -39,6 +59,9 @@ export const metadata: Metadata = {
index: true,
follow: true,
},
other: {
"application/ld+json": JSON.stringify(indianRiverBreadcrumbSchema),
},
};
export default function IndianRiverLayout({ children }: { children: React.ReactNode }) {
+3 -2
View File
@@ -2,6 +2,7 @@
import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { motion, useInView } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
@@ -351,13 +352,13 @@ export default function IndianRiverDirectPage() {
<p className="text-stone-500">Products coming soon for the 2026 season!</p>
</div>
) : (
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-4 sm:gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{products.map((product) => (
<div key={product.id} className="rounded-2xl bg-white border-2 border-stone-200 overflow-hidden shadow-xl hover:shadow-2xl hover:-translate-y-2 hover:border-blue-200 transition-all duration-300">
{/* Product Image */}
<div className="aspect-[4/3] bg-gradient-to-br from-blue-600 to-blue-700 flex items-center justify-center relative">
{product.image_url ? (
<img src={product.image_url} alt={product.name} className="w-full h-full object-cover" />
<Image src={product.image_url} alt={product.name} fill className="object-cover" sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" />
) : (
<div className="text-6xl opacity-30">🍑</div>
)}
+34 -1
View File
@@ -1,9 +1,42 @@
import type { Metadata } from "next";
import PricingClientPage from "./PricingClientPage";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
export const metadata: Metadata = {
title: "Pricing — Route Commerce",
title: "Pricing",
description: "Simple, transparent pricing for produce wholesale operations. Starter at $49/mo, Farm at $149/mo, Enterprise at $399/mo. Built for farms, Co-ops, and produce distributors.",
keywords: ["produce wholesale pricing", "farm software pricing", "agriculture platform", "route commerce plans", "stops scheduling pricing"],
openGraph: {
title: "Pricing — Route Commerce",
description: "Simple, transparent pricing for produce wholesale operations. Starter at $49/mo, Farm at $149/mo, Enterprise at $399/mo.",
url: `${BASE_URL}/pricing`,
siteName: "Route Commerce",
locale: "en_US",
type: "website",
images: [
{
url: "/og-default.jpg",
width: 1200,
height: 630,
alt: "Route Commerce Pricing",
},
],
},
twitter: {
card: "summary_large_image",
title: "Pricing — Route Commerce",
description: "Simple, transparent pricing for produce wholesale operations. Starter at $49/mo, Farm at $149/mo, Enterprise at $399/mo.",
site: "@RouteCommerce",
images: ["/og-default.jpg"],
},
alternates: {
canonical: `${BASE_URL}/pricing`,
},
robots: {
index: true,
follow: true,
},
};
export default function PricingPage() {
+17 -3
View File
@@ -1,12 +1,16 @@
import { MetadataRoute } from "next";
import { getActiveStopsForSitemap } from "@/actions/stops";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yourdomain.com";
export default function sitemap(): MetadataRoute.Sitemap {
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const now = new Date();
return [
// Main site
// Fetch active stops for dynamic URLs
const activeStops = await getActiveStopsForSitemap();
// Base static URLs
const staticUrls: MetadataRoute.Sitemap = [
{
url: BASE_URL,
lastModified: now,
@@ -98,4 +102,14 @@ export default function sitemap(): MetadataRoute.Sitemap {
priority: 0.8,
},
];
// Dynamic stop URLs
const stopUrls: MetadataRoute.Sitemap = activeStops.map((stop) => ({
url: `${BASE_URL}/${stop.brand_slug}/stops/${stop.slug}`,
lastModified: new Date(stop.last_modified),
changeFrequency: "weekly" as const,
priority: 0.8,
}));
return [...staticUrls, ...stopUrls];
}
+44
View File
@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import ContactClientPage from "./ContactClientPage";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
export const metadata: Metadata = {
title: "Contact Us",
description: "Get in touch with Tuxedo Corn. Questions about corn orders, pickup stops, or wholesale accounts — we're happy to help.",
keywords: ["Tuxedo Corn contact", "Olathe Sweet contact", "corn wholesale inquiry", "order questions"],
openGraph: {
title: "Contact Us — Tuxedo Corn",
description: "Get in touch with Tuxedo Corn. Questions about corn orders, pickup stops, or wholesale accounts.",
url: `${BASE_URL}/tuxedo/contact`,
siteName: "Tuxedo Corn",
locale: "en_US",
type: "website",
images: [
{
url: "/og-tuxedo.jpg",
width: 1200,
height: 630,
alt: "Contact Tuxedo Corn",
},
],
},
twitter: {
card: "summary_large_image",
title: "Contact Us — Tuxedo Corn",
description: "Get in touch with Tuxedo Corn. Questions about corn orders, pickup stops, or wholesale accounts.",
site: "@TuxedoCorn",
images: ["/og-tuxedo.jpg"],
},
alternates: {
canonical: `${BASE_URL}/tuxedo/contact`,
},
robots: {
index: true,
follow: true,
},
};
export default function TuxedoContactLayout({ children }: { children: React.ReactNode }) {
return <ContactClientPage />;
}
+99
View File
@@ -0,0 +1,99 @@
import type { Metadata } from "next";
import FAQClientPage from "./FAQClientPage";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
export const metadata: Metadata = {
title: "FAQ — Frequently Asked Questions",
description: "Find answers to common questions about ordering corn, pickup stops, shipping, and wholesale accounts for Tuxedo Corn.",
keywords: ["Tuxedo Corn FAQ", "Olathe Sweet corn questions", "corn pickup FAQ", "wholesale corn accounts", "corn shipping"],
openGraph: {
title: "FAQ — Tuxedo Corn Frequently Asked Questions",
description: "Find answers to common questions about ordering corn, pickup stops, shipping, and wholesale accounts.",
url: `${BASE_URL}/tuxedo/faq`,
siteName: "Tuxedo Corn",
locale: "en_US",
type: "website",
images: [
{
url: "/og-tuxedo.jpg",
width: 1200,
height: 630,
alt: "Tuxedo Corn FAQ",
},
],
},
twitter: {
card: "summary_large_image",
title: "FAQ — Tuxedo Corn",
description: "Find answers to common questions about ordering corn, pickup stops, shipping, and wholesale accounts.",
site: "@TuxedoCorn",
images: ["/og-tuxedo.jpg"],
},
alternates: {
canonical: `${BASE_URL}/tuxedo/faq`,
},
robots: {
index: true,
follow: true,
},
};
const faqJsonLd = {
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How do I place a preorder for corn?",
"acceptedAnswer": {
"@type": "Answer",
"text": "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."
}
},
{
"@type": "Question",
"name": "Can I order without selecting a pickup stop?",
"acceptedAnswer": {
"@type": "Answer",
"text": "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."
}
},
{
"@type": "Question",
"name": "Do you ship corn?",
"acceptedAnswer": {
"@type": "Answer",
"text": "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."
}
},
{
"@type": "Question",
"name": "How do I set up a wholesale account?",
"acceptedAnswer": {
"@type": "Answer",
"text": "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."
}
},
{
"@type": "Question",
"name": "What makes Olathe Sweet different?",
"acceptedAnswer": {
"@type": "Answer",
"text": "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."
}
}
]
};
export default function TuxedoFAQLayout({ children }: { children: React.ReactNode }) {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<FAQClientPage />
</>
);
}
+23
View File
@@ -2,6 +2,26 @@ import type { Metadata } from "next";
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://routecommerce.com";
// BreadcrumbList schema for SEO
const tuxedoBreadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": BASE_URL,
},
{
"@type": "ListItem",
"position": 2,
"name": "Tuxedo Corn",
"item": `${BASE_URL}/tuxedo`,
},
],
};
export const metadata: Metadata = {
title: {
default: "Tuxedo Corn | Fresh Produce Wholesale",
@@ -39,6 +59,9 @@ export const metadata: Metadata = {
index: true,
follow: true,
},
other: {
"application/ld+json": JSON.stringify(tuxedoBreadcrumbSchema),
},
};
export default function TuxedoLayout({ children }: { children: React.ReactNode }) {
+2 -2
View File
@@ -548,7 +548,7 @@ export default function TuxedoPage() {
<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]">
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
Upcoming<br className="hidden md:block" /> Stops
</h2>
<div className="mt-6 h-px w-12 bg-emerald-600" />
@@ -656,7 +656,7 @@ export default function TuxedoPage() {
{ 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="text-3xl sm:text-4xl font-black text-white">{stat}</p>
<p className="mt-1 text-xs text-stone-500 uppercase tracking-widest">{label}</p>
</div>
))}
+3 -3
View File
@@ -195,15 +195,15 @@ export default function AdminOrdersPanel({
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Total</p>
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-text-primary)] mt-1">{orders.length}</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-[var(--admin-text-primary)] mt-1">{orders.length}</p>
</div>
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Pending</p>
<p className="text-xl sm:text-2xl font-bold text-amber-600 mt-1">{pendingCount}</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-amber-600 mt-1">{pendingCount}</p>
</div>
<div className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<p className="text-[10px] sm:text-xs text-[var(--admin-text-muted)] font-medium">Picked Up</p>
<p className="text-xl sm:text-2xl font-bold text-[var(--admin-accent)] mt-1">{pickedUpCount}</p>
<p className="text-lg sm:text-xl md:text-2xl font-bold text-[var(--admin-accent)] mt-1">{pickedUpCount}</p>
</div>
</div>
+1 -1
View File
@@ -187,7 +187,7 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
className="fixed top-4 left-4 z-50 lg:hidden"
aria-label="Open sidebar"
>
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white shadow-md border border-[var(--admin-border)]">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-white shadow-md border border-[var(--admin-border)]">
<HamburgerIcon />
</div>
</button>
+1 -1
View File
@@ -114,7 +114,7 @@ export default function AdminStopsPanel({ stops, brandId }: Props) {
<div className="mx-auto max-w-7xl px-6 py-5">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-stone-950 tracking-tight">Tour Stops</h1>
<h1 className="text-xl sm:text-2xl font-semibold text-stone-950 tracking-tight">Tour Stops</h1>
<div className="mt-2 flex items-center gap-4">
<span className="text-sm text-stone-500">
<span className="font-medium text-stone-700">{activeCount}</span> active
+18 -16
View File
@@ -110,22 +110,24 @@ export default function CommunicationsPage({
/>
{/* Tab navigation */}
<AdminFilterTabs
activeTab={currentTab}
onTabChange={(tab) => {
setCurrentTab(tab as Tab);
setIsComposing(false);
}}
tabs={[
{ value: "campaigns", label: "Campaigns", icon: <MailIcon /> },
{ value: "templates", label: "Templates", icon: <FileTextIcon /> },
{ value: "contacts", label: "Contacts", icon: <UsersIcon /> },
{ value: "segments", label: "Segments", icon: <LayersIcon /> },
{ value: "logs", label: "Logs", icon: <ListIcon /> },
{ value: "analytics", label: "Analytics", icon: <ChartIcon /> },
]}
size="md"
/>
<div className="overflow-x-auto -mx-4 px-4 sm:mx-0 sm:px-0 pb-2">
<AdminFilterTabs
activeTab={currentTab}
onTabChange={(tab) => {
setCurrentTab(tab as Tab);
setIsComposing(false);
}}
tabs={[
{ value: "campaigns", label: "Campaigns", icon: <MailIcon /> },
{ value: "templates", label: "Templates", icon: <FileTextIcon /> },
{ value: "contacts", label: "Contacts", icon: <UsersIcon /> },
{ value: "segments", label: "Segments", icon: <LayersIcon /> },
{ value: "logs", label: "Logs", icon: <ListIcon /> },
{ value: "analytics", label: "Analytics", icon: <ChartIcon /> },
]}
size="md"
/>
</div>
</div>
{/* Content */}
+2 -2
View File
@@ -247,7 +247,7 @@ export default function DashboardClient({
</AdminBadge>
<span className="text-xs text-stone-500">{brandId ? "Tuxedo Corn" : "All Brands"}</span>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-3 gap-2 sm:gap-4">
{[
{ label: "Users", value: `${usage.users}/${limits.max_users}`, pct: usagePct.users },
{ label: "Stops", value: `${usage.stops_this_month}/${limits.max_stops_monthly}`, pct: usagePct.stops },
@@ -286,7 +286,7 @@ export default function DashboardClient({
/>
{/* Section Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
{tabSections.map((section) => {
if (section.title === "Water Log" && !isWaterLogVisible) return null;
if (section.title === "Route Trace" && !enabledAddons["route_trace"]) return null;
+3 -3
View File
@@ -199,7 +199,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
</button>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
<AdminInput label="Qty">
<input
type="number"
@@ -245,7 +245,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
/>
</AdminInput>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AdminInput label="Discount Amount">
<AdminTextInput
type="number"
@@ -282,7 +282,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
onChange={(e) => setCustomer_name(e.target.value)}
/>
</AdminInput>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AdminInput label="Email">
<AdminTextInput
type="email"
+1 -1
View File
@@ -472,7 +472,7 @@ export default function ProductsClient({ products, brandId }: { products: Produc
</>
) : imagePreview ? (
<div className="relative">
<div className="relative h-40 w-auto">
<div className="relative h-32 sm:h-40 w-auto">
<Image src={imagePreview} alt="Product preview" fill style={{ objectFit: "contain" }} className="rounded-lg" />
</div>
<span className="block text-center text-xs text-stone-500 mt-2">Click or drop to replace</span>
@@ -20,7 +20,7 @@ const maxWidthClasses = {
export default function AdminLayout({ children, maxWidth = "2xl", className = "" }: AdminLayoutProps) {
return (
<main className="min-h-screen admin-section" style={{ backgroundColor: "var(--admin-bg)" }}>
<div className={`mx-auto px-6 py-10 ${maxWidthClasses[maxWidth]} ${className}`}>
<div className={`mx-auto px-4 sm:px-6 py-6 sm:py-10 ${maxWidthClasses[maxWidth]} ${className}`}>
{children}
</div>
</main>
@@ -0,0 +1,98 @@
"use client";
import Link from "next/link";
export type BreadcrumbItem = {
label: string;
href?: string;
};
export type BreadcrumbNavProps = {
/** Array of breadcrumb items. Last item is the current page (no href). */
items: BreadcrumbItem[];
/** Optional brand accent for styling: 'green' (Tuxedo) or 'blue' (Indian River) */
brandAccent?: "green" | "blue";
/** Optional additional CSS classes */
className?: string;
};
/**
* Breadcrumb navigation component following Apple HIG.
* Uses semantic <nav> with aria-label and ordered list for accessibility.
*/
export default function BreadcrumbNav({
items,
brandAccent = "green",
className = "",
}: BreadcrumbNavProps) {
if (!items || items.length === 0) {
return null;
}
const accentColor = brandAccent === "blue"
? "text-blue-600 hover:text-blue-700"
: "text-emerald-600 hover:text-emerald-700";
const separatorColor = "text-stone-400";
return (
<nav
aria-label="Breadcrumb"
className={`w-full ${className}`}
>
<ol className="flex items-center flex-wrap gap-2 text-sm">
{items.map((item, index) => {
const isLast = index === items.length - 1;
return (
<li
key={index}
className="flex items-center"
>
{index > 0 && (
<span
className={`mx-2 ${separatorColor}`}
aria-hidden="true"
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
</span>
)}
{isLast ? (
<span
className="font-medium text-stone-700"
aria-current="page"
>
{item.label}
</span>
) : item.href ? (
<Link
href={item.href}
className={`font-medium transition-colors ${accentColor}`}
>
{item.label}
</Link>
) : (
<span className={`font-medium ${accentColor}`}>
{item.label}
</span>
)}
</li>
);
})}
</ol>
</nav>
);
}
+1 -1
View File
@@ -38,7 +38,7 @@ export default function StopCard({
<h3 className="text-2xl font-black text-stone-950 leading-tight tracking-tight">
{city}, {state}
</h3>
<p className="mt-1 text-sm text-stone-500 leading-relaxed line-clamp-1">{location}</p>
<p className="mt-1 text-xs sm:text-sm text-stone-500 leading-relaxed line-clamp-2">{location}</p>
</div>
</div>
@@ -142,7 +142,7 @@ export default function StorefrontHeader({
{/* Mobile nav */}
{menuOpen && (
<div className="border-t border-stone-100 bg-white px-6 py-6 md:hidden">
<div className="border-t border-stone-100 bg-white px-4 py-5 md:hidden">
<nav className="flex flex-col gap-1 text-sm font-medium text-stone-600">
{navLinks.map((link) => (
<Link