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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user