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
+161 -83
View File
@@ -3,7 +3,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { updateOrder, updateOrderItem, deleteOrderItem } from "@/actions/orders/update-order";
import { AdminInput, AdminTextInput, AdminTextarea } from "./design-system";
import { AdminInput, AdminTextInput, AdminTextarea, useToast, AdminButton } from "./design-system";
type OrderItem = {
id: string;
@@ -56,6 +56,7 @@ function formatCurrency(amount: number) {
export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
const router = useRouter();
const { success: showSuccess, error: showError } = useToast();
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [saved, setSaved] = useState(false);
@@ -87,10 +88,34 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
);
const total = Math.max(0, subtotal - discount_amount);
// Validation
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
function validateForm(): boolean {
const errors: Record<string, string> = {};
if (!customer_name.trim()) {
errors.customer_name = "Customer name is required";
}
if (discount_amount < 0) {
errors.discount_amount = "Discount cannot be negative";
}
setFieldErrors(errors);
return Object.keys(errors).length === 0;
}
function updateItem(id: string, field: "quantity" | "price", value: number) {
setItems((prev) =>
prev.map((i) => (i.id === id ? { ...i, [field]: value } : i))
);
// Clear error for the field when changed
setFieldErrors((prev) => {
const next = { ...prev };
delete next[field];
return next;
});
}
function removeItem(id: string) {
@@ -100,6 +125,11 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
}
async function handleSave() {
if (!validateForm()) {
showError("Validation failed", "Please fix the errors below");
return;
}
setSaving(true);
setError(null);
setSaved(false);
@@ -107,72 +137,75 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
const toSave = items.filter((i) => !i.removed);
const toRemove = items.filter((i) => i.removed);
for (const item of toRemove) {
const result = await deleteOrderItem(item.id);
if (!result.success) {
setError(result.error ?? "Failed to remove item");
setSaving(false);
return;
}
}
for (const item of toSave) {
const orig = order.order_items.find((o) => o.id === item.id);
if (!orig) continue;
if (
Number(orig.quantity) !== item.quantity ||
Number(orig.price) !== item.price
) {
const result = await updateOrderItem(item.id, {
quantity: item.quantity,
price: Number(item.price),
});
try {
for (const item of toRemove) {
const result = await deleteOrderItem(item.id);
if (!result.success) {
setError(result.error ?? "Failed to update item");
showError("Failed to remove item", result.error ?? "Please try again");
setSaving(false);
return;
}
}
}
const result = await updateOrder(order.id, brandId, {
customer_name,
customer_email: customer_email || null,
customer_phone: customer_phone || null,
discount_amount,
discount_reason: discount_reason || null,
internal_notes: internal_notes || null,
status,
pickup_complete,
pickup_completed_at: pickup_complete ? new Date().toISOString() : null,
subtotal,
});
for (const item of toSave) {
const orig = order.order_items.find((o) => o.id === item.id);
if (!orig) continue;
if (
Number(orig.quantity) !== item.quantity ||
Number(orig.price) !== item.price
) {
const result = await updateOrderItem(item.id, {
quantity: item.quantity,
price: Number(item.price),
});
if (!result.success) {
showError("Failed to update item", result.error ?? "Please try again");
setSaving(false);
return;
}
}
}
if (!result.success) {
setError(result.error ?? "Failed to save");
const result = await updateOrder(order.id, brandId, {
customer_name,
customer_email: customer_email || null,
customer_phone: customer_phone || null,
discount_amount,
discount_reason: discount_reason || null,
internal_notes: internal_notes || null,
status,
pickup_complete,
pickup_completed_at: pickup_complete ? new Date().toISOString() : null,
subtotal,
});
if (!result.success) {
showError("Failed to save", result.error ?? "Please try again");
setSaving(false);
return;
}
showSuccess("Order updated", "Changes have been saved");
setSaved(true);
setSaving(false);
router.refresh();
} catch (err) {
showError("Network error", "Please check your connection and try again");
setSaving(false);
return;
}
setSaved(true);
setSaving(false);
router.refresh();
}
return (
<div className="space-y-6">
{error && (
<div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-600">
<div className="rounded-xl bg-red-50 border border-red-200 p-4 text-sm text-red-600 flex items-start gap-3">
<svg className="h-5 w-5 shrink-0 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{error}
</div>
)}
{saved && (
<div className="rounded-xl bg-green-50 border border-green-200 p-4 text-sm text-green-700">
Order updated successfully
</div>
)}
{/* Order items */}
<div>
<p className="mb-3 text-sm font-semibold text-stone-700">
@@ -200,7 +233,8 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
</div>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-3">
<AdminInput label="Qty">
<div>
<label className="block text-xs font-medium text-stone-500 mb-1">Qty</label>
<input
type="number"
min="1"
@@ -208,20 +242,25 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
onChange={(e) =>
updateItem(item.id, "quantity", Number(e.target.value))
}
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-sm outline-none focus:border-emerald-500"
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
/>
</AdminInput>
<AdminInput label="Price">
<AdminTextInput
type="number"
step="0.01"
min="0"
value={item.price}
onChange={(e) =>
updateItem(item.id, "price", Number(e.target.value))
}
/>
</AdminInput>
</div>
<div>
<label className="block text-xs font-medium text-stone-500 mb-1">Price</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400 text-sm">$</span>
<input
type="number"
step="0.01"
min="0"
value={item.price}
onChange={(e) =>
updateItem(item.id, "price", Number(e.target.value))
}
className="w-full rounded-xl border border-stone-200 bg-stone-50 pl-8 pr-3 py-2 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
/>
</div>
</div>
</div>
<p className="mt-2 text-right text-sm font-semibold text-stone-900">
@@ -246,22 +285,37 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
</AdminInput>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AdminInput label="Discount Amount">
<AdminTextInput
type="number"
step="0.01"
min="0"
value={discount_amount}
onChange={(e) => setDiscount_amount(Number(e.target.value))}
/>
</AdminInput>
<AdminInput label="Discount Reason">
<AdminTextInput
<div>
<label className="block text-xs font-semibold text-stone-500 mb-1.5">Discount Amount</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400 text-sm">$</span>
<input
type="number"
step="0.01"
min="0"
value={discount_amount}
onChange={(e) => setDiscount_amount(Number(e.target.value))}
className={`w-full rounded-xl border px-8 pr-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/20 ${
fieldErrors.discount_amount
? "border-red-400 bg-red-50"
: "border-stone-200 bg-white focus:border-[var(--admin-accent)]"
}`}
/>
</div>
{fieldErrors.discount_amount && (
<p className="mt-1 text-xs text-red-500">{fieldErrors.discount_amount}</p>
)}
</div>
<div>
<label className="block text-xs font-semibold text-stone-500 mb-1.5">Discount Reason</label>
<input
type="text"
value={discount_reason}
onChange={(e) => setDiscount_reason(e.target.value)}
placeholder="Optional"
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-[var(--admin-accent)] focus:ring-2 focus:ring-[var(--admin-accent)]/20"
/>
</AdminInput>
</div>
</div>
<div className="rounded-xl border border-stone-200 bg-stone-50 p-4">
@@ -276,12 +330,31 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
{/* Customer fields */}
<div className="space-y-4">
<AdminInput label="Name">
<AdminTextInput
<div>
<label className="block text-xs font-semibold text-stone-500 mb-1.5">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={customer_name}
onChange={(e) => setCustomer_name(e.target.value)}
onChange={(e) => {
setCustomer_name(e.target.value);
setFieldErrors((prev) => {
const next = { ...prev };
delete next.customer_name;
return next;
});
}}
className={`w-full rounded-xl border px-3 py-2.5 text-sm outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/20 ${
fieldErrors.customer_name
? "border-red-400 bg-red-50"
: "border-stone-200 bg-white focus:border-[var(--admin-accent)]"
}`}
/>
</AdminInput>
{fieldErrors.customer_name && (
<p className="mt-1 text-xs text-red-500">{fieldErrors.customer_name}</p>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<AdminInput label="Email">
<AdminTextInput
@@ -308,10 +381,11 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
{["pending", "confirmed", "cancelled"].map((s) => (
<button
key={s}
type="button"
onClick={() => setStatus(s)}
className={`flex-1 rounded-xl px-4 py-2.5 text-sm font-semibold capitalize transition-colors ${
status === s
? "bg-emerald-600 text-white"
? "bg-[var(--admin-accent)] text-white"
: "bg-white text-stone-600 border border-stone-200 hover:bg-stone-50"
}`}
>
@@ -324,6 +398,7 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
<div>
<label className="mb-2 block text-xs font-semibold text-stone-500">Pickup</label>
<button
type="button"
onClick={() => setPickup_complete((v) => !v)}
className={`w-full rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors ${
pickup_complete
@@ -346,13 +421,16 @@ export default function OrderEditForm({ order, brandId }: OrderEditFormProps) {
/>
</AdminInput>
<button
<AdminButton
onClick={handleSave}
disabled={saving}
className="w-full rounded-xl bg-emerald-600 px-6 py-3 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50"
isLoading={saving}
variant="primary"
fullWidth
size="lg"
>
{saving ? "Saving..." : "Save Changes"}
</button>
</AdminButton>
</div>
);
}