import { useNumber } from '@/hooks/use-numer-formatter'; import { ModalContent } from '@/modals/Modal/Container'; import { cn } from '@/utils/cn'; import { DialogTitle } from '@radix-ui/react-dialog'; import { useVirtualizer } from '@tanstack/react-virtual'; import { SearchIcon } from 'lucide-react'; import React, { useMemo, useRef, useState } from 'react'; import { Input } from '../ui/input'; const ROW_HEIGHT = 36; // Revenue pie chart component function RevenuePieChart({ percentage }: { percentage: number }) { const size = 16; const strokeWidth = 2; const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const offset = circumference - percentage * circumference; return ( ); } // Base data type that all items must conform to export interface OverviewListItem { sessions: number; pageviews: number; revenue?: number; } interface OverviewListModalProps { /** Modal title */ title: string; /** Search placeholder text */ searchPlaceholder?: string; /** The data to display */ data: T[]; /** Extract a unique key for each item */ keyExtractor: (item: T) => string; /** Filter function for search - receives item and lowercase search query */ searchFilter: (item: T, query: string) => boolean; /** Render the main content cell (first column) */ renderItem: (item: T) => React.ReactNode; /** Optional footer content */ footer?: React.ReactNode; /** Optional header content (appears below title/search) */ headerContent?: React.ReactNode; /** Column name for the first column */ columnName?: string; /** Whether to show pageviews column */ showPageviews?: boolean; /** Whether to show sessions column */ showSessions?: boolean; } export function OverviewListModal({ title, searchPlaceholder = 'Search...', data, keyExtractor, searchFilter, renderItem, footer, headerContent, columnName = 'Name', showPageviews = true, showSessions = true, }: OverviewListModalProps) { const [searchQuery, setSearchQuery] = useState(''); const scrollAreaRef = useRef(null); const number = useNumber(); // Filter data based on search query const filteredData = useMemo(() => { if (!searchQuery.trim()) { return data; } const queryLower = searchQuery.toLowerCase(); return data.filter((item) => searchFilter(item, queryLower)); }, [data, searchQuery, searchFilter]); // Calculate totals and check for revenue const { maxSessions, totalRevenue, hasRevenue, hasPageviews } = useMemo(() => { const maxSessions = Math.max(...filteredData.map((item) => item.sessions)); const totalRevenue = filteredData.reduce( (sum, item) => sum + (item.revenue ?? 0), 0, ); const hasRevenue = filteredData.some((item) => (item.revenue ?? 0) > 0); const hasPageviews = showPageviews && filteredData.some((item) => item.pageviews > 0); return { maxSessions, totalRevenue, hasRevenue, hasPageviews }; }, [filteredData, showPageviews]); // Virtual list setup const virtualizer = useVirtualizer({ count: filteredData.length, getScrollElement: () => scrollAreaRef.current, estimateSize: () => ROW_HEIGHT, overscan: 10, }); const virtualItems = virtualizer.getVirtualItems(); return ( {/* Sticky Header */}
{title}
setSearchQuery(e.target.value)} className="pl-9" />
{headerContent}
{/* Column Headers */}
{columnName}
{hasRevenue &&
Revenue
} {hasPageviews &&
Views
} {showSessions &&
Sessions
}
{/* Virtualized Scrollable Body */}
{virtualItems.map((virtualRow) => { const item = filteredData[virtualRow.index]; if (!item) return null; const percentage = item.sessions / maxSessions; const revenuePercentage = totalRevenue > 0 ? (item.revenue ?? 0) / totalRevenue : 0; return (
{/* Background bar */}
{/* Row content */}
{/* Main content cell */}
{renderItem(item)}
{/* Revenue cell */} {hasRevenue && (
{(item.revenue ?? 0) > 0 ? number.currency((item.revenue ?? 0) / 100, { short: true, }) : '-'}
)} {/* Pageviews cell */} {hasPageviews && (
{number.short(item.pageviews)}
)} {/* Sessions cell */} {showSessions && (
{number.short(item.sessions)}
)}
); })}
{/* Empty state */} {filteredData.length === 0 && (
{searchQuery ? 'No results found' : 'No data available'}
)}
{/* Fixed Footer */} {footer && (
{footer}
)} ); }