From 7455d99ab81adb413d4be3d41320f89b910e6f0f Mon Sep 17 00:00:00 2001 From: dwindown Date: Thu, 20 Nov 2025 16:00:03 +0700 Subject: [PATCH] feat: Add vertical tab layout to Coupon form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented VerticalTabForm component for better UX Created Components: 1. VerticalTabForm.tsx - Reusable vertical tab layout - Left sidebar with navigation (250px on desktop) - Right content area (scrollable) - Scroll spy - auto-highlights active section - Click to scroll to section - Smooth scrolling behavior - Icons support for tabs 2. FormSection component - Wrapper for form sections - Proper ref forwarding - Section ID tracking Updated CouponForm: - Added vertical tab navigation - 3 sections: General, Usage restrictions, Usage limits - Icons: Settings, ShieldCheck, BarChart3 - Narrower content area (better readability) - Desktop-only (lg:block) - mobile keeps original layout Features: ✅ Scroll spy - active tab follows scroll ✅ Click navigation - smooth scroll to section ✅ Visual hierarchy - clear section separation ✅ Better space utilization ✅ Reduced form width for readability ✅ Professional UI like Shopify/Stripe Layout: - Desktop: 250px sidebar + remaining content - Content: max-h-[calc(100vh-200px)] scrollable - Sticky sidebar (top-4) - Active state: bg-primary text-primary-foreground - Hover state: bg-muted hover:text-foreground Next: Apply same pattern to Products form --- admin-spa/src/components/VerticalTabForm.tsx | 132 +++++++++++++++++++ admin-spa/src/components/ui/multi-select.tsx | 5 +- admin-spa/src/routes/Coupons/CouponForm.tsx | 50 ++++--- 3 files changed, 169 insertions(+), 18 deletions(-) create mode 100644 admin-spa/src/components/VerticalTabForm.tsx diff --git a/admin-spa/src/components/VerticalTabForm.tsx b/admin-spa/src/components/VerticalTabForm.tsx new file mode 100644 index 0000000..9e33e4e --- /dev/null +++ b/admin-spa/src/components/VerticalTabForm.tsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { cn } from '@/lib/utils'; + +export interface VerticalTab { + id: string; + label: string; + icon?: React.ReactNode; +} + +interface VerticalTabFormProps { + tabs: VerticalTab[]; + children: React.ReactNode; + className?: string; +} + +export function VerticalTabForm({ tabs, children, className }: VerticalTabFormProps) { + const [activeTab, setActiveTab] = useState(tabs[0]?.id || ''); + const contentRef = useRef(null); + const sectionRefs = useRef<{ [key: string]: HTMLElement }>({}); + + // Scroll spy - update active tab based on scroll position + useEffect(() => { + const handleScroll = () => { + if (!contentRef.current) return; + + const scrollPosition = contentRef.current.scrollTop + 100; // Offset for better UX + + // Find which section is currently in view + for (const tab of tabs) { + const section = sectionRefs.current[tab.id]; + if (section) { + const { offsetTop, offsetHeight } = section; + if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) { + setActiveTab(tab.id); + break; + } + } + } + }; + + const content = contentRef.current; + if (content) { + content.addEventListener('scroll', handleScroll); + return () => content.removeEventListener('scroll', handleScroll); + } + }, [tabs]); + + // Register section refs + const registerSection = (id: string, element: HTMLElement | null) => { + if (element) { + sectionRefs.current[id] = element; + } + }; + + // Scroll to section + const scrollToSection = (id: string) => { + const section = sectionRefs.current[id]; + if (section && contentRef.current) { + const offsetTop = section.offsetTop - 20; // Small offset from top + contentRef.current.scrollTo({ + top: offsetTop, + behavior: 'smooth', + }); + setActiveTab(id); + } + }; + + return ( +
+ {/* Vertical Tabs Sidebar */} +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ + {/* Content Area */} +
+ {React.Children.map(children, (child) => { + if (React.isValidElement(child) && child.props['data-section-id']) { + const sectionId = child.props['data-section-id']; + return React.cloneElement(child as React.ReactElement, { + ref: (el: HTMLElement) => registerSection(sectionId, el), + }); + } + return child; + })} +
+
+ ); +} + +// Section wrapper component for easier usage +interface SectionProps { + id: string; + children: React.ReactNode; + className?: string; +} + +export const FormSection = React.forwardRef( + ({ id, children, className }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +FormSection.displayName = 'FormSection'; diff --git a/admin-spa/src/components/ui/multi-select.tsx b/admin-spa/src/components/ui/multi-select.tsx index 67b65dd..61580d1 100644 --- a/admin-spa/src/components/ui/multi-select.tsx +++ b/admin-spa/src/components/ui/multi-select.tsx @@ -120,7 +120,10 @@ export function MultiSelect({ - + {emptyMessage} {options.map((option) => ( diff --git a/admin-spa/src/routes/Coupons/CouponForm.tsx b/admin-spa/src/routes/Coupons/CouponForm.tsx index 1217bb6..f0b5821 100644 --- a/admin-spa/src/routes/Coupons/CouponForm.tsx +++ b/admin-spa/src/routes/Coupons/CouponForm.tsx @@ -9,7 +9,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Checkbox } from '@/components/ui/checkbox'; import { Button } from '@/components/ui/button'; import { MultiSelect } from '@/components/ui/multi-select'; +import { VerticalTabForm, FormSection } from '@/components/VerticalTabForm'; import { ProductsApi } from '@/lib/api'; +import { Settings, ShieldCheck, BarChart3 } from 'lucide-react'; import type { Coupon, CouponFormData } from '@/lib/api/coupons'; interface CouponFormProps { @@ -76,10 +78,18 @@ export default function CouponForm({ setFormData(prev => ({ ...prev, [field]: value })); }; + const tabs = [ + { id: 'general', label: __('General'), icon: }, + { id: 'restrictions', label: __('Usage restrictions'), icon: }, + { id: 'limits', label: __('Usage limits'), icon: }, + ]; + return ( -
- {/* General Settings */} - + + + {/* General Settings */} + + {__('General')} @@ -170,8 +180,10 @@ export default function CouponForm({ + - {/* Usage Restrictions */} + {/* Usage Restrictions */} + {__('Usage restrictions')} @@ -320,8 +332,10 @@ export default function CouponForm({

+
- {/* Usage Limits */} + {/* Usage Limits */} + {__('Usage limits')} @@ -378,19 +392,21 @@ export default function CouponForm({

+
- {/* Submit Button (if not hidden) */} - {!hideSubmitButton && ( -
- -
- )} + {/* Submit Button (if not hidden) */} + {!hideSubmitButton && ( +
+ +
+ )} +
); }