perf: add export loading state and virtual scrolling for large lists

Export modal now shows a loading indicator and doesn't freeze the UI.
Large list modals use virtual scrolling for smooth performance.
This commit is contained in:
Usman Baig
2026-03-10 20:45:49 +01:00
parent 848bde237f
commit f10b903a80
9 changed files with 546 additions and 413 deletions

View File

@@ -8,6 +8,7 @@ import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeRefer
import { FrameCornersIcon } from '@phosphor-icons/react'
import { Modal, GlobeIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons'
import VirtualList from './VirtualList'
import { getTopReferrers, TopReferrer } from '@/lib/api/stats'
import { type DimensionFilter } from '@/lib/filters'
@@ -165,7 +166,7 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
<div className="max-h-[80vh]">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
@@ -173,26 +174,33 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
) : (() => {
const modalData = mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers).filter(r => !modalSearch || getReferrerDisplayName(r.referrer).toLowerCase().includes(modalSearch.toLowerCase()))
const modalTotal = modalData.reduce((sum, r) => sum + r.pageviews, 0)
return modalData.map((ref) => (
<div
key={ref.referrer}
onClick={() => { if (onFilter) { onFilter({ dimension: 'referrer', operator: 'is', values: [ref.referrer] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
>
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{renderReferrerIcon(ref.referrer)}
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((ref.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(ref.pageviews)}
</span>
</div>
</div>
))
return (
<VirtualList
items={modalData}
estimateSize={36}
className="max-h-[80vh] overflow-y-auto pr-2"
renderItem={(ref) => (
<div
key={ref.referrer}
onClick={() => { if (onFilter) { onFilter({ dimension: 'referrer', operator: 'is', values: [ref.referrer] }); setIsModalOpen(false) } }}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`}
>
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{renderReferrerIcon(ref.referrer)}
<span className="truncate" title={ref.referrer}>{getReferrerDisplayName(ref.referrer)}</span>
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{modalTotal > 0 ? `${Math.round((ref.pageviews / modalTotal) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400">
{formatNumber(ref.pageviews)}
</span>
</div>
</div>
)}
/>
)
})()}
</div>
</Modal>