import React from 'react' import { Globe, Question, DeviceMobile, DeviceTablet, Desktop, Link, CursorClick, } from '@phosphor-icons/react' import { SiGoogle, SiFacebook, SiInstagram, SiGithub, SiYoutube, SiReddit, SiWhatsapp, SiTelegram, SiSnapchat, SiPinterest, SiThreads, SiDuckduckgo, SiBrave, SiPerplexity, SiAnthropic, SiGooglegemini, SiGithubcopilot, SiDiscord, } from '@icons-pack/react-simple-icons' // Inline SVG icons for brands not in @icons-pack/react-simple-icons function XIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) { return } function LinkedInIcon({ size = 16, color = '#0A66C2' }: { size?: number; color?: string }) { return } function OpenAIIcon({ size = 16, color = '#fff' }: { size?: number; color?: string }) { return } function BingIcon({ size = 16, color = '#258FFA' }: { size?: number; color?: string }) { return } /** * 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' const BROWSER_ICON_MAP: Record = { 'chrome': { file: 'chrome', ext: 'svg' }, 'firefox': { file: 'firefox', ext: 'svg' }, 'safari': { file: 'safari', ext: 'svg' }, 'edge': { file: 'edge', ext: 'svg' }, 'opera': { file: 'opera', ext: 'svg' }, 'brave': { file: 'brave', ext: 'svg' }, 'vivaldi': { file: 'vivaldi', ext: 'svg' }, 'samsung internet': { file: 'samsung-internet', ext: 'svg' }, 'uc browser': { file: 'uc-browser', ext: 'svg' }, 'yandex browser': { file: 'yandex', ext: 'png' }, 'waterfox': { file: 'waterfox', ext: 'png' }, 'pale moon': { file: 'pale-moon', ext: 'png' }, 'duckduckgo': { file: 'duckduckgo', ext: 'png' }, 'maxthon': { file: 'maxthon', ext: 'png' }, 'silk': { file: 'silk', ext: 'png' }, 'puffin': { file: 'puffin', ext: 'png' }, 'arc': { file: 'arc', ext: 'png' }, 'tor': { file: 'tor', ext: 'png' }, 'opera mini': { file: 'opera-mini', ext: 'png' }, } export function getBrowserIcon(browserName: string) { if (!browserName) return const entry = BROWSER_ICON_MAP[browserName.toLowerCase()] if (!entry) return const src = `/icons/browsers/${entry.file}.${entry.ext}` return {browserName} } const OS_DARK_INVERT = new Set(['macos', 'playstation']) const OS_ICON_MAP: Record = { 'windows': 'windows', 'macos': 'macos', 'linux': 'linux', 'android': 'android', 'ios': 'ios', 'chromeos': 'chromeos', 'harmonyos': 'harmonyos', 'kaios': 'kaios', 'tizen': 'tizen', 'webos': 'webos', 'freebsd': 'freebsd', 'openbsd': 'openbsd', 'netbsd': 'netbsd', 'playstation': 'playstation', 'xbox': 'xbox', 'nintendo': 'nintendo', } export function getOSIcon(osName: string) { if (!osName) return const file = OS_ICON_MAP[osName.toLowerCase()] if (!file) return const cls = OS_DARK_INVERT.has(file) ? 'inline-block dark:invert' : 'inline-block' return {osName} } 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 } const SI = { size: 16 } as const export function getReferrerIcon(referrerName: string) { if (!referrerName) return const lower = referrerName.toLowerCase() // Direct traffic if (lower === 'direct') return // Browsers as referrers (e.g. googlechrome.com, firefox.com) if (lower.includes('googlechrome') || lower.includes('chrome')) return Chrome // Social / platforms if (lower.includes('google') && !lower.includes('gemini')) return if (lower.includes('facebook') || lower === 'fb') return if (lower.includes('twitter') || lower.includes('t.co') || lower.includes('x.com')) return if (lower.includes('linkedin')) return if (lower.includes('instagram') || lower === 'ig') 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 if (lower.includes('discord')) return // Search engines if (lower.includes('bing')) return if (lower.includes('duckduckgo')) return if (lower.includes('brave')) return // AI assistants 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 // Shared Link 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', bing: 'Bing', brave: 'Brave', duckduckgo: 'DuckDuckGo', whatsapp: 'WhatsApp', telegram: 'Telegram', pinterest: 'Pinterest', snapchat: 'Snapchat', threads: 'Threads', tumblr: 'Tumblr', quora: 'Quora', ig: 'Instagram', fb: 'Facebook', yt: 'YouTube', googlechrome: 'Google Chrome', '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) { // Plain names without a dot (e.g. "Ig", "Direct") — check override map before returning raw const overrideByPlain = REFERRER_DISPLAY_OVERRIDES[trimmed.toLowerCase()] if (overrideByPlain) return overrideByPlain 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/labels where the Phosphor icon is better than Google's favicon service. * For these, getReferrerFavicon returns null so the caller falls back to getReferrerIcon. */ const REFERRER_PREFER_ICON = new Set([ // Social / platforms 't.co', 'x.com', 'twitter.com', 'www.twitter.com', 'google.com', 'www.google.com', 'facebook.com', 'www.facebook.com', 'm.facebook.com', 'l.facebook.com', 'instagram.com', 'www.instagram.com', 'l.instagram.com', 'linkedin.com', 'www.linkedin.com', 'github.com', 'www.github.com', 'youtube.com', 'www.youtube.com', 'm.youtube.com', 'reddit.com', 'www.reddit.com', 'old.reddit.com', 'whatsapp.com', 'www.whatsapp.com', 'web.whatsapp.com', 'telegram.org', 'web.telegram.org', 't.me', 'snapchat.com', 'www.snapchat.com', 'pinterest.com', 'www.pinterest.com', 'threads.net', 'www.threads.net', // Search engines 'bing.com', 'www.bing.com', 'duckduckgo.com', 'www.duckduckgo.com', 'search.brave.com', 'brave.com', // AI assistants 'chatgpt.com', 'chat.openai.com', 'openai.com', 'perplexity.ai', 'www.perplexity.ai', 'claude.ai', 'www.claude.ai', 'anthropic.com', 'gemini.google.com', 'copilot.microsoft.com', 'deepseek.com', 'chat.deepseek.com', 'grok.x.ai', 'x.ai', 'phind.com', 'www.phind.com', 'you.com', 'www.you.com', ]) /** * Returns a favicon URL for the referrer's domain, or null for non-URL referrers * (e.g. "Direct", "Unknown") or known services where the Phosphor icon is better. */ 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}`) const hostname = url.hostname.toLowerCase() // Use Phosphor icon for known services — Google favicons are unreliable for these if (REFERRER_PREFER_ICON.has(hostname)) return null // Also check if the label matches a known referrer (catches subdomains like search.google.com) const label = getReferrerLabel(hostname) if (REFERRER_DISPLAY_OVERRIDES[label]) return null return `${FAVICON_SERVICE_URL}?domain=${hostname}&sz=32` } catch { return null } }