import React from 'react' import { Globe, WindowsLogo, AppleLogo, LinuxLogo, AndroidLogo, Question, DeviceMobile, DeviceTablet, Desktop, GoogleLogo, FacebookLogo, XLogo, LinkedinLogo, InstagramLogo, GithubLogo, YoutubeLogo, RedditLogo, Robot, Link, WhatsappLogo, TelegramLogo, SnapchatLogo, PinterestLogo, ThreadsLogo, } from '@phosphor-icons/react' /** * Google's public favicon service base URL. * Append `?domain=&sz=` to get a favicon. */ export const FAVICON_SERVICE_URL = 'https://www.google.com/s2/favicons' export function getBrowserIcon(browserName: string) { if (!browserName) return return } export function getOSIcon(osName: string) { if (!osName) return const lower = osName.toLowerCase() if (lower.includes('win')) return if (lower.includes('mac') || lower.includes('ios')) return if (lower.includes('linux') || lower.includes('ubuntu') || lower.includes('debian')) return if (lower.includes('android')) return return } export function getDeviceIcon(deviceName: string) { if (!deviceName) return const lower = deviceName.toLowerCase() if (lower.includes('mobile') || lower.includes('phone')) return if (lower.includes('tablet') || lower.includes('ipad')) return if (lower.includes('desktop') || lower.includes('laptop')) return return } export function getReferrerIcon(referrerName: string) { if (!referrerName) return const lower = referrerName.toLowerCase() if (lower.includes('google')) return if (lower.includes('facebook')) return if (lower.includes('twitter') || lower.includes('t.co') || lower.includes('x.com')) return if (lower.includes('linkedin')) return if (lower.includes('instagram')) return if (lower.includes('github')) return if (lower.includes('youtube')) return if (lower.includes('reddit')) return if (lower.includes('whatsapp')) return if (lower.includes('telegram')) return if (lower.includes('snapchat')) return if (lower.includes('pinterest')) return if (lower.includes('threads')) return // AI assistants and search tools if (lower.includes('chatgpt') || lower.includes('openai')) return if (lower.includes('perplexity')) return if (lower.includes('claude') || lower.includes('anthropic')) return if (lower.includes('gemini')) return if (lower.includes('copilot')) return if (lower.includes('deepseek')) return if (lower.includes('grok') || lower.includes('x.ai')) return if (lower.includes('phind')) return if (lower.includes('you.com')) return // Shared Link (unattributed deep-page traffic) if (lower === 'shared link') return return } const REFERRER_NO_FAVICON = ['direct', 'shared link', 'unknown', ''] /** Common subdomains to skip when deriving the main label (e.g. l.instagram.com → instagram). */ const REFERRER_SUBDOMAIN_SKIP = new Set([ 'www', 'm', 'l', 'app', 'mobile', 'search', 'mail', 'drive', 'maps', 'docs', 'sub', 'api', 'static', 'cdn', 'blog', 'shop', 'support', 'help', 'link', ]) /** * Override map for display names when the heuristic would be wrong (casing or brand alias). * Keys: lowercase label or hostname. Values: exact display name. */ const REFERRER_DISPLAY_OVERRIDES: Record = { chatgpt: 'ChatGPT', linkedin: 'LinkedIn', youtube: 'YouTube', reddit: 'Reddit', github: 'GitHub', duckduckgo: 'DuckDuckGo', whatsapp: 'WhatsApp', telegram: 'Telegram', pinterest: 'Pinterest', snapchat: 'Snapchat', threads: 'Threads', tumblr: 'Tumblr', quora: 'Quora', 't.co': 'X', 'x.com': 'X', // AI assistants and search tools openai: 'ChatGPT', perplexity: 'Perplexity', claude: 'Claude', anthropic: 'Claude', gemini: 'Gemini', copilot: 'Copilot', deepseek: 'DeepSeek', grok: 'Grok', 'you': 'You.com', phind: 'Phind', } /** * Returns the hostname for a referrer string (URL or plain hostname), or null if invalid. */ function getReferrerHostname(referrer: string): string | null { if (!referrer || typeof referrer !== 'string') return null const trimmed = referrer.trim() if (REFERRER_NO_FAVICON.includes(trimmed.toLowerCase())) return null try { const url = new URL(trimmed.startsWith('http') ? trimmed : `https://${trimmed}`) return url.hostname.toLowerCase() } catch { return null } } /** * Derives the main label from a hostname (e.g. "l.instagram.com" → "instagram", "google.com" → "google"). */ function getReferrerLabel(hostname: string): string { const withoutWww = hostname.startsWith('www.') ? hostname.slice(4) : hostname const parts = withoutWww.split('.') if (parts.length >= 2 && REFERRER_SUBDOMAIN_SKIP.has(parts[0])) { return parts[1] } return parts[0] ?? withoutWww } function capitalizeLabel(label: string): string { if (!label) return label return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase() } /** * Returns a friendly display name for the referrer (e.g. "Google" instead of "google.com"). * Uses a heuristic (hostname → main label → capitalize) plus a small override map for famous brands. */ export function getReferrerDisplayName(referrer: string): string { if (!referrer || typeof referrer !== 'string') return referrer || '' const trimmed = referrer.trim() if (trimmed === '') return '' const hostname = getReferrerHostname(trimmed) if (!hostname) return trimmed const overrideByHostname = REFERRER_DISPLAY_OVERRIDES[hostname] if (overrideByHostname) return overrideByHostname const label = getReferrerLabel(hostname) const overrideByLabel = REFERRER_DISPLAY_OVERRIDES[label] if (overrideByLabel) return overrideByLabel return capitalizeLabel(label) } /** * Merges referrer rows that share the same display name (e.g. chatgpt.com and https://chatgpt.com/...), * summing pageviews and keeping one referrer per group for icon/tooltip. Sorted by pageviews desc. */ export function mergeReferrersByDisplayName( items: Array<{ referrer: string; pageviews: number }> ): Array<{ referrer: string; pageviews: number }> { const byDisplayName = new Map() for (const ref of items) { const name = getReferrerDisplayName(ref.referrer) const existing = byDisplayName.get(name) if (!existing) { byDisplayName.set(name, { referrer: ref.referrer, pageviews: ref.pageviews, maxSingle: ref.pageviews }) } else { existing.pageviews += ref.pageviews if (ref.pageviews > existing.maxSingle) { existing.maxSingle = ref.pageviews existing.referrer = ref.referrer } } } return Array.from(byDisplayName.values()) .map(({ referrer, pageviews }) => ({ referrer, pageviews })) .sort((a, b) => b.pageviews - a.pageviews) } /** Domains that always use the custom X icon instead of favicon (avoids legacy bird). */ const REFERRER_USE_X_ICON = new Set(['t.co', 'x.com', 'twitter.com', 'www.twitter.com']) /** * Returns a favicon URL for the referrer's domain, or null for non-URL referrers * (e.g. "Direct", "Unknown") so callers can show an icon fallback instead. */ export function getReferrerFavicon(referrer: string): string | null { if (!referrer || typeof referrer !== 'string') return null const normalized = referrer.trim().toLowerCase() if (REFERRER_NO_FAVICON.includes(normalized)) return null // Plain names without a dot (e.g. "Instagram", "WhatsApp") are not real domains if (!normalized.includes('.')) return null try { const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`) if (REFERRER_USE_X_ICON.has(url.hostname.toLowerCase())) return null return `${FAVICON_SERVICE_URL}?domain=${url.hostname}&sz=32` } catch { return null } }