Fixed 3 critical issues: 1. Fixed Vertical Tabs - Cards All Showing - Updated VerticalTabForm to hide inactive sections - Only active section visible (className: hidden for others) - Proper tab switching now works 2. Added Mobile Search/Filter to Coupons - Created CouponFilterSheet component - Added mobile search bar with icon - Filter button with active count badge - Matches Products pattern exactly - Sheet with Apply/Reset buttons 3. Removed max-height from VerticalTabForm - User removed max-h-[calc(100vh-200px)] - Content now flows naturally - Better for forms with varying heights Components Created: - CouponFilterSheet.tsx - Mobile filter bottom sheet - Discount type filter - Apply/Reset actions - Active filter count Changes to Coupons/index.tsx: - Added mobile search bar (md:hidden) - Added filter sheet state - Added activeFiltersCount - Search icon + SlidersHorizontal icon - Filter badge indicator Changes to VerticalTabForm: - Hide inactive sections (className: hidden) - Only show section matching activeTab - Proper visibility control Result: ✅ Vertical tabs work correctly (only one section visible) ✅ Mobile search/filter on Coupons (like Products) ✅ Filter count badge ✅ Professional mobile UX Next: Move customer site member checkbox to settings
135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
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<HTMLDivElement>(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 (
|
|
<div className={cn('flex gap-6', className)}>
|
|
{/* Vertical Tabs Sidebar */}
|
|
<div className="hidden lg:block w-56 flex-shrink-0">
|
|
<div className="sticky top-4 space-y-1">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => scrollToSection(tab.id)}
|
|
className={cn(
|
|
'w-full text-left px-4 py-2.5 rounded-md text-sm font-medium transition-colors',
|
|
'flex items-center gap-3',
|
|
activeTab === tab.id
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
)}
|
|
>
|
|
{tab.icon && <span className="w-4 h-4">{tab.icon}</span>}
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div
|
|
ref={contentRef}
|
|
className="flex-1 overflow-y-auto pr-2"
|
|
>
|
|
{React.Children.map(children, (child) => {
|
|
if (React.isValidElement(child) && child.props['data-section-id']) {
|
|
const sectionId = child.props['data-section-id'];
|
|
const isActive = sectionId === activeTab;
|
|
return React.cloneElement(child as React.ReactElement<any>, {
|
|
ref: (el: HTMLElement) => registerSection(sectionId, el),
|
|
className: isActive ? '' : 'hidden',
|
|
});
|
|
}
|
|
return child;
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Section wrapper component for easier usage
|
|
interface SectionProps {
|
|
id: string;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export const FormSection = React.forwardRef<HTMLDivElement, SectionProps>(
|
|
({ id, children, className }, ref) => {
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
data-section-id={id}
|
|
className={cn('mb-6 scroll-mt-4', className)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
|
|
FormSection.displayName = 'FormSection';
|