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
@@ -0,0 +1,106 @@
"use client";
type AdminToggleProps = {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
size?: "sm" | "md";
label?: string;
description?: string;
className?: string;
};
export function AdminToggle({
checked,
onChange,
disabled = false,
size = "md",
label,
description,
className = "",
}: AdminToggleProps) {
const sizeClasses = {
sm: {
track: "h-6 w-11",
thumb: "h-4 w-4",
translate: checked ? "translate-x-5" : "translate-x-1",
},
md: {
track: "h-7 w-12",
thumb: "h-5 w-5",
translate: checked ? "translate-x-6" : "translate-x-1",
},
};
const classes = sizeClasses[size];
return (
<label className={`flex items-start gap-3 ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"} ${className}`}>
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={`relative inline-flex shrink-0 items-center rounded-full transition-colors ${
checked
? "bg-[var(--admin-accent)]"
: "bg-[var(--admin-text-muted)]"
} ${classes.track}`}
>
<span
className={`inline-block rounded-full bg-white shadow transition-transform ${classes.thumb} ${classes.translate}`}
/>
</button>
{(label || description) && (
<div className="flex flex-col">
{label && (
<span className="text-sm font-medium text-[var(--admin-text-primary)]">{label}</span>
)}
{description && (
<span className="text-xs text-[var(--admin-text-muted)]">{description}</span>
)}
</div>
)}
</label>
);
}
// Compact toggle for use within form rows (no label, smaller)
export function AdminToggleCompact({
checked,
onChange,
disabled = false,
label,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
label?: string;
}) {
return (
<div className="flex items-center gap-2">
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${
checked ? "bg-[var(--admin-accent)]" : "bg-[var(--admin-text-muted)]"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
checked ? "translate-x-5" : "translate-x-1"
}`}
/>
</button>
{label && (
<span className="text-sm text-[var(--admin-text-secondary)]">{label}</span>
)}
</div>
);
}
export default AdminToggle;
@@ -0,0 +1,160 @@
"use client";
// Shimmer skeleton components for loading states
// Uses CSS animation for smooth shimmer effect
interface SkeletonProps {
className?: string;
variant?: "text" | "rect" | "circle" | "card";
width?: string | number;
height?: string | number;
count?: number;
}
export function Skeleton({ className = "", variant = "rect", width, height }: SkeletonProps) {
const variantClasses = {
text: "h-4 rounded",
rect: "rounded-xl",
circle: "rounded-full",
card: "rounded-xl",
};
const style: React.CSSProperties = {};
if (width) style.width = typeof width === "number" ? `${width}px` : width;
if (height) style.height = typeof height === "number" ? `${height}px` : height;
return (
<div
className={`animate-pulse bg-gradient-to-r from-stone-200 via-stone-100 to-stone-200 bg-[length:200%_100%] shimmer ${variantClasses[variant]} ${className}`}
style={style}
/>
);
}
// Table row skeleton
export function SkeletonTable({ rows = 5, cols = 5 }: { rows?: number; cols?: number }) {
return (
<div className="space-y-3">
{Array.from({ length: rows }).map((_, rowIndex) => (
<div key={rowIndex} className="flex items-center gap-4 p-4">
{Array.from({ length: cols }).map((_, colIndex) => (
<Skeleton key={colIndex} variant="text" className="flex-1" />
))}
</div>
))}
</div>
);
}
// Card skeleton for grid views
export function SkeletonCard({ showImage = true }: { showImage?: boolean }) {
return (
<div className="rounded-xl border border-[var(--admin-border)] bg-white overflow-hidden">
{showImage && <Skeleton className="h-40 w-full" />}
<div className="p-4 space-y-3">
<Skeleton variant="text" className="w-3/4 h-5" />
<Skeleton variant="text" className="w-1/2 h-4" />
<div className="flex justify-between pt-2">
<Skeleton variant="text" className="w-1/4 h-6" />
<Skeleton variant="rect" className="w-16 h-8" />
</div>
</div>
</div>
);
}
// Stats card skeleton
export function SkeletonStats({ count = 3 }: { count?: number }) {
return (
<div className={`grid grid-cols-${Math.min(count, 4)} gap-3`}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<Skeleton variant="text" className="w-1/3 h-3 mb-2" />
<Skeleton variant="text" className="w-1/2 h-6" />
</div>
))}
</div>
);
}
// Full page loading skeleton
export function PageSkeleton() {
return (
<div className="p-4 sm:p-6 space-y-6">
{/* Header skeleton */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Skeleton variant="circle" className="h-10 w-10" />
<div className="space-y-2">
<Skeleton variant="text" className="w-32 h-6" />
<Skeleton variant="text" className="w-24 h-4" />
</div>
</div>
<Skeleton variant="rect" className="w-28 h-10" />
</div>
{/* Stats skeleton */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-white rounded-xl border border-[var(--admin-border)] p-4">
<Skeleton variant="text" className="w-1/2 h-3 mb-2" />
<Skeleton variant="text" className="w-1/3 h-7" />
</div>
))}
</div>
{/* Filter bar skeleton */}
<div className="flex gap-3">
<Skeleton variant="rect" className="h-10 flex-1 max-w-xs" />
<Skeleton variant="rect" className="h-10 w-32" />
<Skeleton variant="rect" className="h-10 w-24" />
</div>
{/* Table skeleton */}
<div className="rounded-xl border border-[var(--admin-border)] bg-white overflow-hidden">
<div className="p-4 border-b border-[var(--admin-border)]">
<div className="flex gap-4">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} variant="text" className="h-4 w-20" />
))}
</div>
</div>
<div className="divide-y divide-[var(--admin-border)]">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="p-4 flex gap-4">
<Skeleton variant="text" className="w-20 h-4" />
<Skeleton variant="text" className="w-32 h-4" />
<Skeleton variant="text" className="w-24 h-4 flex-1" />
<Skeleton variant="text" className="w-16 h-4" />
<Skeleton variant="text" className="w-16 h-4" />
</div>
))}
</div>
</div>
{/* Pagination skeleton */}
<div className="flex items-center justify-between pt-4">
<Skeleton variant="text" className="w-32 h-4" />
<div className="flex gap-2">
<Skeleton variant="rect" className="h-9 w-9" />
<Skeleton variant="rect" className="h-9 w-12" />
<Skeleton variant="rect" className="h-9 w-9" />
</div>
</div>
</div>
);
}
// Form skeleton
export function FormSkeleton() {
return (
<div className="space-y-6 p-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton variant="text" className="w-24 h-3" />
<Skeleton variant="rect" className="h-10 w-full" />
</div>
))}
</div>
);
}
@@ -0,0 +1,6 @@
// Toast notification system - re-exports for design-system
// The actual implementations are in the parent admin directory
// Use: import { ToastProvider, useToast, useToastActions, ToastContainer } from "@/components/admin/Toast";
export { ToastProvider, useToast, useToastActions } from "@/components/admin/Toast";
export { ToastContainer, InlineToast } from "@/components/admin/ToastContainer";
@@ -20,6 +20,14 @@ export { default as AdminFilterTabs, AdminStatusFilterTabs, AdminViewModeTabs }
// Form elements
export { AdminInput, AdminTextInput, AdminTextarea, AdminSelect, AdminCheckbox, AdminSpinner, AdminLoadingOverlay } from "./AdminFormElements";
export { AdminToggle, AdminToggleCompact } from "./AdminToggle";
// Skeleton loading components
export { Skeleton, SkeletonTable, SkeletonCard, SkeletonStats, PageSkeleton, FormSkeleton } from "./Skeleton";
// Toast notification system
export { ToastProvider, useToast, useToastActions } from "@/components/admin/Toast";
export { ToastContainer, InlineToast } from "@/components/admin/ToastContainer";
// Modal component - GlassModal is the standard modal (max-w-lg default), AdminModal is a smaller variant (max-w-md default)
export { default as GlassModal } from "@/components/admin/GlassModal";