feat: comprehensive frontend polish - UI/UX improvements across all pages

Public Pages:
- Landing page with server/client split and metadata export
- Cart page with ARIA accessibility and loading states
- Checkout page with form accessibility and proper design system
- Contact page with SEO metadata and improved accessibility
- Pricing page with enhanced FAQ accessibility
- Login page with Suspense boundaries and secure patterns

Brand Storefronts:
- Premium loading skeletons for Tuxedo and Indian River Direct
- Branded error boundaries with animations
- Loading.tsx for about, contact, FAQ, stops pages
- Error.tsx for all storefront subpages

Admin Dashboard:
- AdminSidebar: ARIA labels, keyboard navigation, mobile improvements
- DashboardClient: Stats cards, quick actions, usage progress
- Admin layout: Toast provider integration

Admin Orders/Products/Stops:
- Toast notification system with auto-dismiss
- Skeleton loading components (Table, Card, Stats, Form)
- Bulk actions (mark picked up, publish stops)
- Form validation with error styling

Admin Communications:
- AnalyticsDashboard: sparklines, stat cards, engagement badges
- CampaignComposerPage: step wizard, template selection, email preview
- SegmentBuilderPage: empty state, active segment handling
- ContactListPanel: loading skeletons, professional empty states
- MessageLogPanel: stats cards, engagement indicators
- HarvestReachNav: branded tab navigation
- All pages: metadata exports and loading.tsx

Admin Settings:
- SquareSyncSettingsClient: design system colors, save/cancel UX
- ShippingSettingsForm: validation, dirty state tracking
- Integrations page: proper layout with AI and communications sections
- Billing: improved plan comparison, add-on cards
- PaymentSettings: toggle components, validation

Wholesale Portal:
- Portal: loading skeletons, quantity stepper, search/filter
- Login/Register: FormField validation, success states
- Success/Cancel pages: animated checkmarks
- Employee portal: skeletons, empty states, mobile responsive

Water Log:
- FieldClient: loading step, progress indicator, spinner
- AdminClient: loading skeletons
- Admin pages: loading.tsx files

New Components:
- Toast.tsx/ToastContainer.tsx: comprehensive notification system
- Skeleton.tsx: shimmer loading components
- AdminToggle.tsx: consistent toggle/switch
- CommunicationsLoading.tsx: loading skeleton for comms
- ToastExport.ts: exports

CSS Improvements:
- Shimmer animation keyframes
- Toast slide-in animation

Accessibility:
- ARIA labels throughout
- Keyboard navigation
- Focus states
- Semantic HTML
- Screen reader support
This commit is contained in:
2026-06-02 05:19:34 +00:00
parent fb25c5ee22
commit b845d69aba
97 changed files with 10698 additions and 3487 deletions
+130 -73
View File
@@ -3,6 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext";
type StorefrontHeaderProps = {
@@ -17,25 +18,31 @@ type StorefrontHeaderProps = {
const ACCENT_CONFIG = {
green: {
wholesaleBg: "bg-emerald-700 hover:bg-emerald-800",
wholesaleBg: "bg-emerald-700 hover:bg-emerald-800 active:bg-emerald-900",
wholesaleText: "text-white",
cartBg: "bg-stone-900 hover:bg-stone-800",
cartBg: "bg-stone-900 hover:bg-stone-800 active:bg-stone-950",
cartText: "text-white",
cartBadge: "bg-emerald-600",
navHover: "hover:text-emerald-600",
activeNav: "text-emerald-600",
},
orange: {
wholesaleBg: "bg-orange-500 hover:bg-orange-600",
wholesaleBg: "bg-orange-500 hover:bg-orange-600 active:bg-orange-700",
wholesaleText: "text-white",
cartBg: "bg-stone-800 hover:bg-stone-700",
cartBg: "bg-stone-800 hover:bg-stone-700 active:bg-stone-900",
cartText: "text-white",
cartBadge: "bg-orange-500",
navHover: "hover:text-orange-500",
activeNav: "text-orange-500",
},
blue: {
wholesaleBg: "bg-blue-600 hover:bg-blue-500",
wholesaleBg: "bg-blue-600 hover:bg-blue-500 active:bg-blue-700",
wholesaleText: "text-white",
cartBg: "bg-stone-800 hover:bg-stone-700",
cartBg: "bg-stone-800 hover:bg-stone-700 active:bg-stone-900",
cartText: "text-white",
cartBadge: "bg-blue-600",
navHover: "hover:text-blue-600",
activeNav: "text-blue-600",
},
};
@@ -51,12 +58,10 @@ export default function StorefrontHeader({
const { cart } = useCart();
const [menuOpen, setMenuOpen] = useState(false);
const accent = ACCENT_CONFIG[brandAccent] ?? ACCENT_CONFIG.green;
const isBlue = brandAccent === "blue";
const isOrange = brandAccent === "orange";
const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0);
const navLinks = [
const navLinks = [
{ label: "Home", href: `/${brandSlug}` },
{ label: "Stops", href: `/${brandSlug}#stops` },
{ label: "Products", href: `/${brandSlug}#products` },
@@ -64,108 +69,160 @@ export default function StorefrontHeader({
{ label: "FAQ", href: `/${brandSlug}/faq` },
];
const headerBg = isBlue
? "bg-white/95 border-stone-200 shadow-sm"
: isOrange
? "bg-white/95 border-stone-200 shadow-sm"
: "bg-white/95 border-stone-200 shadow-sm";
const navColor = "text-stone-600";
const navHover = "hover:text-blue-600";
const headerBg = "bg-white/95 backdrop-blur-md border-b border-stone-200/60 shadow-sm";
return (
<header className={`sticky top-0 z-40 border-b backdrop-blur-md ${headerBg}`}>
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
{/* Brand */}
<header className={`sticky top-0 z-40 ${headerBg}`}>
<div className="mx-auto flex max-w-6xl items-center justify-between px-5 sm:px-6 py-3 sm:py-4">
{/* Brand Logo */}
<Link href={`/${brandSlug}`} className="flex items-center gap-3 group">
{logoUrlDark ? (
<span className="relative h-10 w-[160px]">
<Image src={logoUrlDark} alt={brandName} fill className="object-contain object-left" />
<span className="relative h-9 sm:h-10 w-[140px] sm:w-[160px]">
<Image src={logoUrlDark} alt={brandName} fill className="object-contain object-left transition-transform duration-300 group-hover:scale-[1.02]" />
</span>
) : logoUrl ? (
<span className="relative h-10 w-[160px]">
<Image src={logoUrl} alt={brandName} fill className="object-contain object-left" />
<span className="relative h-9 sm:h-10 w-[140px] sm:w-[160px]">
<Image src={logoUrl} alt={brandName} fill className="object-contain object-left transition-transform duration-300 group-hover:scale-[1.02]" />
</span>
) : (
<span className="text-xl font-bold tracking-tight text-stone-800">{brandName}</span>
<span className="text-lg sm:text-xl font-bold tracking-tight text-stone-800 group-hover:text-stone-900 transition-colors">
{brandName}
</span>
)}
</Link>
{/* Desktop nav */}
<nav className={`hidden items-center gap-7 text-sm font-medium ${navColor} md:flex`}>
{/* Desktop Navigation */}
<nav className="hidden items-center gap-6 sm:gap-7 text-sm font-medium text-stone-600 md:flex">
{navLinks.map((link) => (
<Link key={link.href} href={link.href} className={`${navHover} transition-colors duration-150`}>
{link.label}
<Link
key={link.href}
href={link.href}
className={`relative py-1 ${accent.navHover} transition-all duration-200 group`}
>
<span className="relative z-10">{link.label}</span>
<span className={`absolute -bottom-0.5 left-0 w-0 h-0.5 ${accent.activeNav.replace('hover:', '')} transition-all duration-200 group-hover:w-full rounded-full`} />
</Link>
))}
{showWholesaleLink && (
<Link
href="/wholesale/login"
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition-colors ${accent.wholesaleBg} ${accent.wholesaleText}`}
className={`rounded-full px-4 py-1.5 text-xs font-semibold transition-all duration-200 ${accent.wholesaleBg} ${accent.wholesaleText} hover:shadow-md hover:shadow-black/10`}
>
Wholesale
</Link>
)}
</nav>
{/* Right side */}
<div className="flex items-center gap-2">
{/* Right Side - Cart & Mobile Menu */}
<div className="flex items-center gap-2 sm:gap-3">
{/* Cart Button */}
<Link
href="/cart"
className={`relative rounded-full px-4 py-2 text-sm font-semibold transition-colors ${accent.cartBg} ${accent.cartText}`}
className={`relative rounded-full px-4 sm:px-5 py-2.5 sm:py-2 text-sm font-semibold transition-all duration-200 ${accent.cartBg} ${accent.cartText} hover:-translate-y-0.5 hover:shadow-lg hover:shadow-black/10 active:scale-95`}
>
Cart
<span className="hidden sm:inline">Cart</span>
<svg className="h-5 w-5 sm:hidden" fill="none" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
{cartCount > 0 && (
<span className={`absolute -top-1.5 -right-1.5 flex h-5 w-5 items-center justify-center rounded-full ${accent.cartBadge} text-[10px] font-bold text-white`}>
<motion.span
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className={`absolute -top-1.5 -right-1.5 flex h-5 w-5 items-center justify-center rounded-full ${accent.cartBadge} text-[10px] font-bold text-white shadow-sm`}
>
{cartCount > 9 ? "9+" : cartCount}
</span>
</motion.span>
)}
</Link>
{/* Mobile menu button */}
{/* Mobile Menu Button */}
<button
onClick={() => setMenuOpen(!menuOpen)}
className="flex h-10 w-10 items-center justify-center rounded-xl border border-stone-200 text-stone-500 hover:border-stone-400 hover:text-stone-700 transition-colors md:hidden"
className="flex h-10 w-10 items-center justify-center rounded-xl border border-stone-200 text-stone-500 transition-all duration-200 hover:border-stone-400 hover:text-stone-700 active:scale-95 md:hidden"
aria-expanded={menuOpen}
aria-label="Toggle menu"
>
{menuOpen ? (
<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>
) : (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
<motion.div
animate={menuOpen ? "open" : "closed"}
className="relative w-5 h-4"
>
<motion.span
animate={menuOpen ? { y: 8, rotate: 45 } : { y: 0, rotate: 0 }}
className="absolute top-0 left-0 w-full h-0.5 bg-current rounded-full"
transition={{ duration: 0.2 }}
/>
<motion.span
animate={menuOpen ? { opacity: 0 } : { opacity: 1 }}
className="absolute top-1/2 left-0 w-full h-0.5 bg-current -translate-y-1/2 rounded-full"
transition={{ duration: 0.2 }}
/>
<motion.span
animate={menuOpen ? { y: -8, rotate: -45 } : { y: 0, rotate: 0 }}
className="absolute bottom-0 left-0 w-full h-0.5 bg-current rounded-full"
transition={{ duration: 0.2 }}
/>
</motion.div>
</button>
</div>
</div>
{/* Mobile nav */}
{menuOpen && (
<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
key={link.href}
href={link.href}
onClick={() => setMenuOpen(false)}
className="py-2.5 transition-colors hover:text-stone-900"
>
{link.label}
</Link>
))}
{showWholesaleLink && (
<Link
href="/wholesale/login"
onClick={() => setMenuOpen(false)}
className="mt-2 py-2.5 font-semibold text-emerald-700 hover:text-emerald-800 transition-colors"
>
Wholesale Portal
</Link>
)}
</nav>
</div>
)}
{/* Mobile Navigation */}
<AnimatePresence>
{menuOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden border-t border-stone-100 bg-white md:hidden"
>
<motion.nav
initial={{ y: -10 }}
animate={{ y: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col gap-1 px-5 py-5"
>
{navLinks.map((link, index) => (
<motion.div
key={link.href}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
>
<Link
href={link.href}
onClick={() => setMenuOpen(false)}
className="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-600 font-medium transition-all duration-200 hover:bg-stone-50 hover:text-stone-900"
>
{link.label}
<svg className="h-4 w-4 ml-auto text-stone-400" fill="none" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</Link>
</motion.div>
))}
{showWholesaleLink && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: navLinks.length * 0.05 }}
>
<Link
href="/wholesale/login"
onClick={() => setMenuOpen(false)}
className="mt-3 flex items-center gap-3 py-3 px-4 rounded-xl bg-emerald-50 text-emerald-700 font-semibold transition-all duration-200 hover:bg-emerald-100"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Wholesale Portal
</Link>
</motion.div>
)}
</motion.nav>
</motion.div>
)}
</AnimatePresence>
</header>
);
}