'use client' import { useState, useEffect } from 'react' import { logger } from '@/lib/utils/logger' import { formatNumber } from '@ciphera-net/ui' import { getReferrerDisplayName, getReferrerFavicon, getReferrerIcon, mergeReferrersByDisplayName } from '@/lib/utils/icons' import Link from 'next/link' import { ArrowSquareOut, FrameCornersIcon } from '@phosphor-icons/react' import { Modal, GlobeIcon, ArrowRightIcon } 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' interface TopReferrersProps { referrers: Array<{ referrer: string; pageviews: number }> collectReferrers?: boolean siteId: string dateRange: { start: string, end: string } onFilter?: (filter: DimensionFilter) => void } const LIMIT = 7 export default function TopReferrers({ referrers, collectReferrers = true, siteId, dateRange, onFilter }: TopReferrersProps) { const [isModalOpen, setIsModalOpen] = useState(false) const [modalSearch, setModalSearch] = useState('') const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) const [faviconFailed, setFaviconFailed] = useState>(new Set()) // Filter out empty/unknown referrers const filteredReferrers = (referrers || []).filter( ref => ref.referrer && ref.referrer !== 'Unknown' && ref.referrer !== '' ) const mergedReferrers = mergeReferrersByDisplayName(filteredReferrers) const totalPageviews = mergedReferrers.reduce((sum, r) => sum + r.pageviews, 0) const hasData = mergedReferrers.length > 0 const displayedReferrers = hasData ? mergedReferrers.slice(0, LIMIT) : [] const emptySlots = Math.max(0, LIMIT - displayedReferrers.length) const showViewAll = hasData && mergedReferrers.length > LIMIT function renderReferrerIcon(referrer: string) { const faviconUrl = getReferrerFavicon(referrer) const useFavicon = faviconUrl && !faviconFailed.has(referrer) if (useFavicon) { return ( // eslint-disable-next-line @next/next/no-img-element setFaviconFailed((prev) => new Set(prev).add(referrer))} onLoad={(e) => { // Google's favicon service returns a 16x16 default globe when no real favicon exists const img = e.currentTarget if (img.naturalWidth <= 16) { setFaviconFailed((prev) => new Set(prev).add(referrer)) } }} /> ) } return {getReferrerIcon(referrer)} } useEffect(() => { if (isModalOpen) { const fetchData = async () => { setIsLoadingFull(true) try { const data = await getTopReferrers(siteId, dateRange.start, dateRange.end, 100) // Filter fetched data too const filtered = (data || []).filter( ref => ref.referrer && ref.referrer !== 'Unknown' && ref.referrer !== '' ) setFullData(filtered) } catch (e) { logger.error(e) } finally { setIsLoadingFull(false) } } fetchData() } else { setFullData([]) } }, [isModalOpen, siteId, dateRange]) return ( <>

Referrers

{showViewAll && ( )}
{!collectReferrers ? (

Referrer tracking is disabled in site settings

) : hasData ? ( <> {displayedReferrers.map((ref) => { const maxPv = displayedReferrers[0]?.pageviews ?? 0 const barWidth = maxPv > 0 ? (ref.pageviews / maxPv) * 75 : 0 return (
onFilter?.({ dimension: 'referrer', operator: 'is', values: ref.allReferrers ?? [ref.referrer] })} className={`relative flex items-center justify-between h-9 group hover:bg-neutral-50/50 dark:hover:bg-neutral-800/50 rounded-lg px-2 -mx-2 transition-colors${onFilter ? ' cursor-pointer' : ''}`} >
{renderReferrerIcon(ref.referrer)} {getReferrerDisplayName(ref.referrer)}
{totalPageviews > 0 ? `${Math.round((ref.pageviews / totalPageviews) * 100)}%` : ''} {formatNumber(ref.pageviews)}
) })} {Array.from({ length: emptySlots }).map((_, i) => (
{ setIsModalOpen(false); setModalSearch('') }} title="Referrers" className="max-w-2xl" >
setModalSearch(e.target.value)} placeholder="Search referrers..." 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-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" />
{isLoadingFull ? (
) : (() => { 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 ( (
{ 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' : ''}`} >
{renderReferrerIcon(ref.referrer)} {getReferrerDisplayName(ref.referrer)}
{modalTotal > 0 ? `${Math.round((ref.pageviews / modalTotal) * 100)}%` : ''} {formatNumber(ref.pageviews)}
)} /> ) })()}
) }