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:
@@ -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");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import CommunicationsLoading from "@/components/admin/CommunicationsLoading";
|
||||
|
||||
export default function Loading() {
|
||||
return <CommunicationsLoading activeTab="campaigns" />;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user