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
+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