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
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
import AbandonedCartDashboard from "@/components/admin/AbandonedCartDashboard";
@@ -5,6 +6,11 @@ import AbandonedCartDashboard from "@/components/admin/AbandonedCartDashboard";
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
const IRD_BRAND_ID = "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28";
export const metadata: Metadata = {
title: "Abandoned Cart Recovery - Harvest Reach",
description: "Recover abandoned carts with automated email sequences.",
};
export default async function AbandonedCartsPage() {
const adminUser = await getAdminUser();
const brandId = adminUser?.brand_id ?? TUXEDO_BRAND_ID;
@@ -13,7 +19,7 @@ export default async function AbandonedCartsPage() {
const brandName = settingsResult?.success ? (settingsResult.settings?.brand_name ?? "Farm") : "Farm";
return (
<main className="min-h-screen px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
<main className="min-h-screen px-4 sm:px-6 md:px-8 py-6 sm:py-8" style={{ backgroundColor: "var(--admin-bg)" }}>
<div className="mx-auto max-w-5xl space-y-6">
{/* Header */}
<div>
@@ -24,12 +30,21 @@ export default async function AbandonedCartsPage() {
<span>/</span>
<span className="text-stone-600">Abandoned Cart Recovery</span>
</nav>
<div className="flex items-end justify-between">
<div>
<h1 className="text-2xl font-bold text-stone-950">Abandoned Cart Recovery</h1>
<p className="mt-1 text-sm text-stone-500">{brandName} 3-email sequence (1h, 24h, 48h)</p>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-amber-500 to-amber-600 shadow-lg shadow-amber-500/20">
<svg className="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="9" cy="21" r="1"/>
<circle cx="20" cy="21" r="1"/>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
</svg>
</div>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-stone-900">Abandoned Cart Recovery</h1>
<p className="text-sm text-stone-500">{brandName} 3-email sequence (1h, 24h, 48h)</p>
</div>
</div>
<div className="flex items-center gap-3 text-xs text-stone-500">
<div className="flex items-center gap-4 text-xs text-stone-500">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-blue-600" />
Active
@@ -50,4 +65,4 @@ export default async function AbandonedCartsPage() {
</div>
</main>
);
}
}
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Analytics - Harvest Reach",
description: "Track campaign performance and email engagement metrics.",
};
export default function AnalyticsPage() {
redirect("/admin/communications");
redirect("/admin/communications?tab=analytics");
}
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getCommunicationCampaigns, getCampaignById } from "@/actions/communications/campaigns";
@@ -5,11 +6,16 @@ import { getCommunicationTemplates } from "@/actions/communications/templates";
import { getHarvestReachSegments } from "@/actions/harvest-reach/segments";
import CommunicationsPage from "@/components/admin/CommunicationsPage";
export default async function CampaignEditPage({
params,
}: {
export const metadata: Metadata = {
title: "Edit Campaign - Harvest Reach",
description: "Edit an existing email campaign.",
};
interface PageProps {
params: Promise<{ id: string }>;
}) {
}
export default async function CampaignEditPage({ params }: PageProps) {
const adminUser = await getAdminUser();
if (!adminUser || !adminUser.can_manage_messages) {
redirect("/admin/pickup");
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getCommunicationCampaigns } from "@/actions/communications/campaigns";
@@ -5,6 +6,11 @@ import { getCommunicationTemplates } from "@/actions/communications/templates";
import { getHarvestReachSegments } from "@/actions/harvest-reach/segments";
import CommunicationsPage from "@/components/admin/CommunicationsPage";
export const metadata: Metadata = {
title: "Compose Campaign - Harvest Reach",
description: "Create and send email campaigns to your customers.",
};
export default async function ComposePage() {
const adminUser = await getAdminUser();
if (!adminUser || !adminUser.can_manage_messages) {
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Contacts - Harvest Reach",
description: "Manage your email marketing contacts and subscriber list.",
};
export default function ContactsPage() {
redirect("/admin/communications");
redirect("/admin/communications?tab=contacts");
}
+5
View File
@@ -0,0 +1,5 @@
import CommunicationsLoading from "@/components/admin/CommunicationsLoading";
export default function Loading() {
return <CommunicationsLoading activeTab="campaigns" />;
}
+7 -1
View File
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Message Logs - Harvest Reach",
description: "View delivery logs and engagement tracking for sent messages.",
};
export default function LogsPage() {
redirect("/admin/communications");
redirect("/admin/communications?tab=logs");
}
+6
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getCommunicationCampaigns } from "@/actions/communications/campaigns";
@@ -6,6 +7,11 @@ import { getHarvestReachSegments } from "@/actions/harvest-reach/segments";
import { getCampaignAnalytics } from "@/actions/harvest-reach/campaigns";
import CommunicationsPage from "@/components/admin/CommunicationsPage";
export const metadata: Metadata = {
title: "Harvest Reach - Communications Hub",
description: "Manage email campaigns, templates, contacts, and marketing automation for your brand.",
};
export default async function CommunicationsRootPage() {
const adminUser = await getAdminUser();
if (!adminUser || !adminUser.can_manage_messages) {
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Segments - Harvest Reach",
description: "Create and manage audience segments for targeted campaigns.",
};
export default function SegmentsPage() {
redirect("/admin/communications");
redirect("/admin/communications?tab=segments");
}
+13 -7
View File
@@ -1,8 +1,14 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getCommunicationSettings } from "@/actions/communications/settings";
import CommunicationSettingsForm from "@/components/admin/CommunicationSettingsForm";
export const metadata: Metadata = {
title: "Settings - Harvest Reach",
description: "Configure email and SMS integration settings for Harvest Reach.",
};
export default async function SettingsPage() {
const adminUser = await getAdminUser();
if (!adminUser) redirect("/admin");
@@ -17,7 +23,7 @@ export default async function SettingsPage() {
{/* Back button */}
<a
href="/admin/communications"
className="inline-flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700 mb-4"
className="inline-flex items-center gap-2 text-sm text-stone-500 hover:text-stone-700 mb-4 transition-colors"
>
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 18-6-6 6-6"/>
@@ -27,20 +33,20 @@ export default async function SettingsPage() {
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-600">
<svg className="h-5 w-5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-emerald-500 to-emerald-600 shadow-lg shadow-emerald-500/20">
<svg className="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
</div>
<div>
<h1 className="text-xl sm:text-2xl font-black text-[var(--admin-text-primary)] tracking-tight">Harvest Reach Settings</h1>
<p className="text-xs text-[var(--admin-text-muted)]">Configure email and SMS integration</p>
<h1 className="text-xl sm:text-2xl font-bold text-stone-900">Harvest Reach Settings</h1>
<p className="text-sm text-stone-500">Configure email and SMS integration</p>
</div>
</div>
{/* Settings Form */}
<div className="rounded-2xl border border-[var(--admin-border)] bg-white p-4 sm:p-6">
<div className="rounded-2xl border border-stone-200 bg-white p-4 sm:p-6 shadow-sm">
<CommunicationSettingsForm settings={settingsResult} brandId={brandId} />
</div>
</div>
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getCommunicationTemplates, getTemplateById } from "@/actions/communications/templates";
@@ -5,11 +6,16 @@ import { getCommunicationCampaigns } from "@/actions/communications/campaigns";
import { getHarvestReachSegments } from "@/actions/harvest-reach/segments";
import CommunicationsPage from "@/components/admin/CommunicationsPage";
export default async function TemplateEditPage({
params,
}: {
export const metadata: Metadata = {
title: "Edit Template - Harvest Reach",
description: "Edit an email template for your marketing campaigns.",
};
interface PageProps {
params: Promise<{ id: string }>;
}) {
}
export default async function TemplateEditPage({ params }: PageProps) {
const adminUser = await getAdminUser();
if (!adminUser || !adminUser.can_manage_messages) {
redirect("/admin/pickup");
@@ -1,5 +1,11 @@
import type { Metadata } from "next";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Templates - Harvest Reach",
description: "Manage email templates for your marketing campaigns.",
};
export default function TemplatesPage() {
redirect("/admin/communications");
redirect("/admin/communications?tab=templates");
}
@@ -1,9 +1,15 @@
import type { Metadata } from "next";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
import WelcomeSequenceDashboard from "@/components/admin/WelcomeSequenceDashboard";
const TUXEDO_BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
export const metadata: Metadata = {
title: "Welcome Sequence - Harvest Reach",
description: "Configure the automated welcome email sequence for new subscribers.",
};
export default async function WelcomeSequencePage() {
const adminUser = await getAdminUser();
const brandId = adminUser?.brand_id ?? TUXEDO_BRAND_ID;
@@ -12,7 +18,7 @@ export default async function WelcomeSequencePage() {
const brandName = settingsResult?.success ? (settingsResult.settings?.brand_name ?? "Farm") : "Farm";
return (
<main className="min-h-screen px-6 py-10" style={{ backgroundColor: "var(--admin-bg)" }}>
<main className="min-h-screen px-4 sm:px-6 md:px-8 py-6 sm:py-8" style={{ backgroundColor: "var(--admin-bg)" }}>
<div className="mx-auto max-w-5xl space-y-6">
{/* Header */}
<div>
@@ -23,10 +29,20 @@ export default async function WelcomeSequencePage() {
<span>/</span>
<span className="text-stone-600">Welcome Sequence</span>
</nav>
<div className="flex items-end justify-between">
<div>
<h1 className="text-2xl font-bold text-stone-950">Welcome Email Sequence</h1>
<p className="mt-1 text-sm text-stone-500">{brandName} 4-email onboarding series</p>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-purple-500 to-purple-600 shadow-lg shadow-purple-500/20">
<svg className="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
</div>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-stone-900">Welcome Email Sequence</h1>
<p className="text-sm text-stone-500">{brandName} 4-email onboarding series</p>
</div>
</div>
<div className="text-xs text-stone-400">
Auto-enrolls new subscribers (email opt-in)
@@ -38,4 +54,4 @@ export default async function WelcomeSequencePage() {
</div>
</main>
);
}
}