import React, { type ReactNode } 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 } import { FAVICON_SERVICE_URL } from './favicon' export { FAVICON_SERVICE_URL } // ─── Browser, OS, Device icons (unchanged) ─────────────────────────────────── 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 } // ─── Referrer Registry ─────────────────────────────────────────────────────── const SI = { size: 16 } as const interface ReferrerEntry { display: string icon: () => ReactNode hostnames?: string[] aliases?: string[] } /** * Single source of truth for all known referrer brands. * Key = canonical label (what getReferrerLabel extracts from hostnames). * Adding a new brand = adding one entry here. Nothing else. */ const REFERRER_REGISTRY: Record = { // ── Special ── direct: { display: 'Direct', icon: () => }, 'shared link': { display: 'Shared Link', icon: () => }, // ── Social / platforms ── google: { display: 'Google', icon: () => }, facebook: { display: 'Facebook', icon: () => , aliases: ['fb'] }, x: { display: 'X', icon: () => , hostnames: ['t.co', 'x.com', 'twitter.com'] }, linkedin: { display: 'LinkedIn', icon: () => }, instagram: { display: 'Instagram', icon: () => , aliases: ['ig'] }, github: { display: 'GitHub', icon: () => }, youtube: { display: 'YouTube', icon: () => , aliases: ['yt'] }, reddit: { display: 'Reddit', icon: () => }, whatsapp: { display: 'WhatsApp', icon: () => }, telegram: { display: 'Telegram', icon: () => , hostnames: ['t.me'] }, snapchat: { display: 'Snapchat', icon: () => }, pinterest: { display: 'Pinterest', icon: () => }, threads: { display: 'Threads', icon: () => }, discord: { display: 'Discord', icon: () => }, tumblr: { display: 'Tumblr', icon: () => }, quora: { display: 'Quora', icon: () => }, // ── Search engines ── bing: { display: 'Bing', icon: () => }, duckduckgo: { display: 'DuckDuckGo', icon: () => }, brave: { display: 'Brave', icon: () => }, // ── AI assistants ── chatgpt: { display: 'ChatGPT', icon: () => , hostnames: ['chat.openai.com', 'openai.com'] }, perplexity: { display: 'Perplexity', icon: () => }, claude: { display: 'Claude', icon: () => , hostnames: ['anthropic.com'] }, gemini: { display: 'Gemini', icon: () => , hostnames: ['gemini.google.com'] }, copilot: { display: 'Copilot', icon: () => , hostnames: ['copilot.microsoft.com'] }, deepseek: { display: 'DeepSeek', icon: () => , hostnames: ['chat.deepseek.com'] }, grok: { display: 'Grok', icon: () => , hostnames: ['grok.x.ai', 'x.ai'] }, you: { display: 'You.com', icon: () => }, phind: { display: 'Phind', icon: () => }, // ── Browsers as referrers ── googlechrome: { display: 'Google Chrome', icon: () => Chrome, hostnames: ['googlechrome.github.io'] }, // ── Ciphera products ── pulse: { display: 'Pulse', icon: () => Pulse, hostnames: ['pulse.ciphera.net', 'pulse-staging.ciphera.net'] }, } // ── Derived lookup maps (built once at module load) ── /** alias → registry key (e.g. "ig" → "instagram", "fb" → "facebook") */ const ALIAS_TO_KEY: Record = {} /** exact hostname → registry key (e.g. "t.co" → "x", "t.me" → "telegram") */ const HOSTNAME_TO_KEY: Record = {} /** All known hostnames — union of auto-derived (key + ".com") and explicit hostnames */ const ALL_KNOWN_HOSTNAMES = new Set() for (const [key, entry] of Object.entries(REFERRER_REGISTRY)) { if (entry.aliases) { for (const alias of entry.aliases) { ALIAS_TO_KEY[alias] = key } } if (entry.hostnames) { for (const hostname of entry.hostnames) { HOSTNAME_TO_KEY[hostname] = key ALL_KNOWN_HOSTNAMES.add(hostname) } } // Auto-derive common hostnames from the key itself ALL_KNOWN_HOSTNAMES.add(`${key}.com`) ALL_KNOWN_HOSTNAMES.add(`www.${key}.com`) } // ── Referrer resolution ── /** 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', ]) const REFERRER_NO_FAVICON = new Set(['direct', 'shared link', 'unknown', '']) /** * 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 getReferrerHostname(referrer: string): string | null { if (!referrer || typeof referrer !== 'string') return null const trimmed = referrer.trim() if (REFERRER_NO_FAVICON.has(trimmed.toLowerCase())) return null try { const url = new URL(trimmed.startsWith('http') ? trimmed : `https://${trimmed}`) return url.hostname.toLowerCase() } catch { return null } } /** * Resolves a raw referrer string to a registry entry. * Returns null if no known brand matches (unknown domain → use favicon service). */ function resolveReferrer(referrer: string): ReferrerEntry | null { if (!referrer || typeof referrer !== 'string') return null const lower = referrer.trim().toLowerCase() // 1. Exact registry key match (e.g. "Direct", "Reddit", "Google") if (REFERRER_REGISTRY[lower]) return REFERRER_REGISTRY[lower] // 2. Alias match (e.g. "ig" → instagram, "fb" → facebook) const aliasKey = ALIAS_TO_KEY[lower] if (aliasKey) return REFERRER_REGISTRY[aliasKey] // 3. Hostname-based matching const hostname = getReferrerHostname(referrer) if (!hostname) return null // 3a. Exact hostname match (e.g. "t.co" → x, "t.me" → telegram) const hostnameKey = HOSTNAME_TO_KEY[hostname] if (hostnameKey) return REFERRER_REGISTRY[hostnameKey] // 3b. Label-based lookup (e.g. "old.reddit.com" → label "reddit" → registry hit) const label = getReferrerLabel(hostname) if (REFERRER_REGISTRY[label]) return REFERRER_REGISTRY[label] // 3c. Check alias from label (e.g. hostname "ig.something.com" → label "ig" → alias → instagram) const labelAliasKey = ALIAS_TO_KEY[label] if (labelAliasKey) return REFERRER_REGISTRY[labelAliasKey] return null } // ── Public API (same signatures as before) ── export function getReferrerIcon(referrerName: string): ReactNode { if (!referrerName) return const entry = resolveReferrer(referrerName) if (entry) return entry.icon() return } function capitalizeLabel(label: string): string { if (!label) return label return label.charAt(0).toUpperCase() + label.slice(1).toLowerCase() } export function getReferrerDisplayName(referrer: string): string { if (!referrer || typeof referrer !== 'string') return referrer || '' const trimmed = referrer.trim() if (trimmed === '') return '' const entry = resolveReferrer(trimmed) if (entry) return entry.display // Unknown referrer — derive display name from hostname const hostname = getReferrerHostname(trimmed) if (!hostname) return trimmed return capitalizeLabel(getReferrerLabel(hostname)) } export function getReferrerFavicon(referrer: string): string | null { if (!referrer || typeof referrer !== 'string') return null const normalized = referrer.trim().toLowerCase() if (REFERRER_NO_FAVICON.has(normalized)) return null if (!normalized.includes('.')) return null // Known brand → skip favicon service, use registry icon if (resolveReferrer(referrer)) return null try { const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`) return `${FAVICON_SERVICE_URL}?domain=${url.hostname.toLowerCase()}&sz=32` } catch { return null } } /** * 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; allReferrers: string[] }> { 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, allReferrers: new Set([ref.referrer]) }) } else { existing.pageviews += ref.pageviews existing.allReferrers.add(ref.referrer) if (ref.pageviews > existing.maxSingle) { existing.maxSingle = ref.pageviews existing.referrer = ref.referrer } } } return Array.from(byDisplayName.values()) .map(({ referrer, pageviews, allReferrers }) => ({ referrer, pageviews, allReferrers: Array.from(allReferrers) })) .sort((a, b) => b.pageviews - a.pageviews) }