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
}
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
}
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: () =>
, hostnames: ['googlechrome.github.io'] },
}
// ── 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 }> {
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)
}