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,35 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
export default function PaymentCancelPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 flex items-center justify-center px-4">
|
||||
<div className="bg-white rounded-2xl shadow-sm ring-1 ring-slate-200 p-8 max-w-md w-full text-center">
|
||||
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-stone-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 rounded-full border-3 border-stone-300 border-t-stone-600 animate-spin mx-auto mb-4" />
|
||||
<p className="text-stone-500">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<PaymentCancelContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function PaymentCancelContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const reason = searchParams.get("reason");
|
||||
const sessionId = searchParams.get("session_id");
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-stone-50 flex items-center justify-center px-4 py-8">
|
||||
<div className="bg-white rounded-3xl shadow-xl ring-1 ring-slate-200 p-8 sm:p-10 max-w-md w-full text-center">
|
||||
{/* Cancel icon */}
|
||||
<div className="w-20 h-20 bg-stone-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<svg className="w-10 h-10 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 mb-2">Payment Cancelled</h1>
|
||||
<p className="text-sm text-slate-500 mb-6">
|
||||
Your payment was not completed. No charges have been made.
|
||||
</p>
|
||||
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 mb-3">
|
||||
Payment Cancelled
|
||||
</h1>
|
||||
|
||||
<div className="bg-stone-50 rounded-xl px-4 py-3 mb-6">
|
||||
<p className="text-sm text-stone-600 font-medium">
|
||||
No charges were made
|
||||
</p>
|
||||
<p className="text-xs text-stone-500 mt-1">
|
||||
Your order is still pending and you can return to complete payment anytime.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sessionId && (
|
||||
<p className="text-xs text-slate-400 mb-4 font-mono">
|
||||
Session: {sessionId.slice(0, 20)}...
|
||||
</p>
|
||||
)}
|
||||
|
||||
{reason && (
|
||||
<p className="text-xs text-amber-600 bg-amber-50 rounded-lg px-3 py-2 mb-6">
|
||||
{reason === "aborted" ? "Payment was aborted by user." : reason}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/wholesale/portal?tab=orders"
|
||||
className="block w-full rounded-xl bg-slate-800 py-3 text-sm font-bold text-white hover:bg-slate-700"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-xl bg-slate-800 py-3.5 text-sm font-bold text-white hover:bg-slate-700 active:bg-slate-800 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
||||
</svg>
|
||||
Back to Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="/wholesale/portal?tab=products"
|
||||
className="block w-full rounded-xl border border-slate-300 py-3 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
href="/wholesale/portal?tab=cart"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-xl border border-slate-300 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-50 active:bg-slate-100 transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
Return to Cart
|
||||
</Link>
|
||||
<Link
|
||||
href="/wholesale/portal?tab=products"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-xl bg-stone-100 py-3 text-sm font-semibold text-stone-600 hover:bg-stone-200 active:bg-stone-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
Browse Products
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-slate-100">
|
||||
<p className="text-xs text-stone-400">
|
||||
Need help?{" "}
|
||||
<Link href="/wholesale/login" className="text-slate-500 hover:text-slate-700 hover:underline">
|
||||
Contact support
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,15 @@
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { enqueueWholesaleNotification } from "@/actions/wholesale";
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-screen bg-slate-100 flex items-center justify-center">
|
||||
<p className="text-slate-500">Loading...</p>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-stone-100 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 rounded-full border-3 border-stone-300 border-t-stone-600 animate-spin mx-auto mb-4" />
|
||||
<p className="text-stone-500">Processing payment...</p>
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<PaymentSuccessContent />
|
||||
@@ -20,55 +22,102 @@ export default function PaymentSuccessPage() {
|
||||
function PaymentSuccessContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const sessionId = searchParams.get("session_id");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [notified, setNotified] = useState(false);
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [successStep, setSuccessStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
// Stagger the animation
|
||||
const timer1 = setTimeout(() => setShowContent(true), 200);
|
||||
const timer2 = setTimeout(() => setSuccessStep(1), 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer1);
|
||||
clearTimeout(timer2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// After render, enqueue deposit received notification
|
||||
// We don't have order details here (webhook handled the DB update),
|
||||
// so we defer to the admin fulfill notification for now.
|
||||
// The webhook already processed the payment — a follow-up cron can
|
||||
// detect deposit_paid increases and send the deposit notification.
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 flex items-center justify-center px-4">
|
||||
<div className="bg-white rounded-2xl shadow-sm ring-1 ring-slate-200 p-8 max-w-md w-full text-center">
|
||||
{loading ? (
|
||||
<p className="text-slate-500">Confirming payment...</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 mb-2">Payment Successful</h1>
|
||||
<p className="text-sm text-slate-500 mb-6">
|
||||
Your payment has been received. Your order will be updated shortly.
|
||||
{sessionId && <span className="block mt-1 text-xs text-slate-400">Session: {sessionId.slice(0, 20)}...</span>}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/wholesale/portal?tab=orders"
|
||||
className="block w-full rounded-xl bg-green-600 py-3 text-sm font-bold text-white hover:bg-green-700"
|
||||
>
|
||||
View Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="/wholesale/portal?tab=products"
|
||||
className="block w-full rounded-xl border border-slate-300 py-3 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-slate-400">
|
||||
A confirmation email will be sent once your payment is processed.
|
||||
</p>
|
||||
</>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-stone-50 flex items-center justify-center px-4 py-8">
|
||||
<div
|
||||
className={`bg-white rounded-3xl shadow-xl ring-1 ring-slate-200 p-8 sm:p-10 max-w-md w-full text-center transition-all duration-500 ${
|
||||
showContent ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
|
||||
}`}
|
||||
>
|
||||
{/* Success animation */}
|
||||
<div
|
||||
className={`relative w-20 h-20 mx-auto mb-6 transition-all duration-500 ${
|
||||
successStep >= 1 ? "scale-100" : "scale-50"
|
||||
}`}
|
||||
>
|
||||
<div className={`absolute inset-0 rounded-full bg-green-100 transition-all duration-500 ${successStep >= 1 ? "opacity-100 scale-100" : "opacity-0 scale-50"}`} />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<svg
|
||||
className={`w-10 h-10 text-green-600 transition-all duration-300 ${
|
||||
successStep >= 1 ? "opacity-100 scale-100" : "opacity-0 scale-50"
|
||||
}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Ripple effect */}
|
||||
<div className="absolute inset-0 rounded-full border-2 border-green-400 animate-ping opacity-20" />
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 mb-3">
|
||||
Payment Successful
|
||||
</h1>
|
||||
|
||||
<div className="bg-green-50 rounded-xl px-4 py-3 mb-6">
|
||||
<p className="text-sm text-green-700 font-medium">
|
||||
Your payment has been received
|
||||
</p>
|
||||
<p className="text-xs text-green-600 mt-1">
|
||||
Your order will be updated shortly. A confirmation email will be sent once your payment is processed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sessionId && (
|
||||
<p className="text-xs text-slate-400 mb-6 font-mono">
|
||||
Reference: {sessionId.slice(0, 20)}...
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Link
|
||||
href="/wholesale/portal?tab=orders"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-xl bg-green-600 py-3.5 text-sm font-bold text-white hover:bg-green-700 active:bg-green-800 transition-colors shadow-lg shadow-green-900/20"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
||||
</svg>
|
||||
View My Orders
|
||||
</Link>
|
||||
<Link
|
||||
href="/wholesale/portal?tab=products"
|
||||
className="flex items-center justify-center gap-2 w-full rounded-xl border border-slate-300 py-3 text-sm font-semibold text-slate-700 hover:bg-slate-50 active:bg-slate-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-slate-100">
|
||||
<Link
|
||||
href="/wholesale/portal"
|
||||
className="text-xs text-slate-400 hover:text-slate-600 transition-colors inline-flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
Back to Portal
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user