Display proper brand icons and names for AI referrers (ChatGPT, Perplexity, Claude, Gemini, Copilot, DeepSeek, Grok, Meta AI, You.com, Phind) in Top Referrers panel.
233 lines
9.5 KiB
TypeScript
233 lines
9.5 KiB
TypeScript
import React from 'react'
|
|
|
|
/**
|
|
* Google's public favicon service base URL.
|
|
* Append `?domain=<host>&sz=<px>` to get a favicon.
|
|
*/
|
|
export const FAVICON_SERVICE_URL = 'https://www.google.com/s2/favicons'
|
|
import {
|
|
FaChrome,
|
|
FaFirefox,
|
|
FaSafari,
|
|
FaEdge,
|
|
FaOpera,
|
|
FaInternetExplorer,
|
|
FaWindows,
|
|
FaApple,
|
|
FaLinux,
|
|
FaAndroid,
|
|
FaDesktop,
|
|
FaMobileAlt,
|
|
FaTabletAlt,
|
|
FaGoogle,
|
|
FaFacebook,
|
|
FaLinkedin,
|
|
FaInstagram,
|
|
FaGithub,
|
|
FaYoutube,
|
|
FaReddit,
|
|
FaQuestion,
|
|
FaGlobe
|
|
} from 'react-icons/fa'
|
|
import { FaXTwitter } from 'react-icons/fa6'
|
|
import { SiBrave, SiOpenai, SiPerplexity, SiAnthropic, SiGooglegemini } from 'react-icons/si'
|
|
import { RiRobot2Fill } from 'react-icons/ri'
|
|
import { MdDeviceUnknown, MdSmartphone, MdTabletMac, MdDesktopWindows } from 'react-icons/md'
|
|
|
|
export function getBrowserIcon(browserName: string) {
|
|
if (!browserName) return <FaGlobe className="text-neutral-400" />
|
|
const lower = browserName.toLowerCase()
|
|
if (lower.includes('chrome')) return <FaChrome className="text-blue-500" />
|
|
if (lower.includes('firefox')) return <FaFirefox className="text-orange-500" />
|
|
if (lower.includes('safari')) return <FaSafari className="text-blue-400" />
|
|
if (lower.includes('edge')) return <FaEdge className="text-blue-600" />
|
|
if (lower.includes('opera')) return <FaOpera className="text-red-500" />
|
|
if (lower.includes('ie') || lower.includes('explorer')) return <FaInternetExplorer className="text-blue-500" />
|
|
if (lower.includes('brave')) return <SiBrave className="text-orange-600" />
|
|
|
|
return <FaGlobe className="text-neutral-400" />
|
|
}
|
|
|
|
export function getOSIcon(osName: string) {
|
|
if (!osName) return <MdDeviceUnknown className="text-neutral-400" />
|
|
const lower = osName.toLowerCase()
|
|
if (lower.includes('win')) return <FaWindows className="text-blue-500" />
|
|
if (lower.includes('mac') || lower.includes('ios')) return <FaApple className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('linux') || lower.includes('ubuntu') || lower.includes('debian')) return <FaLinux className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('android')) return <FaAndroid className="text-green-500" />
|
|
|
|
return <MdDeviceUnknown className="text-neutral-400" />
|
|
}
|
|
|
|
export function getDeviceIcon(deviceName: string) {
|
|
if (!deviceName) return <MdDeviceUnknown className="text-neutral-400" />
|
|
const lower = deviceName.toLowerCase()
|
|
if (lower.includes('mobile') || lower.includes('phone')) return <MdSmartphone className="text-neutral-500" />
|
|
if (lower.includes('tablet') || lower.includes('ipad')) return <MdTabletMac className="text-neutral-500" />
|
|
if (lower.includes('desktop') || lower.includes('laptop')) return <MdDesktopWindows className="text-neutral-500" />
|
|
|
|
return <MdDeviceUnknown className="text-neutral-400" />
|
|
}
|
|
|
|
export function getReferrerIcon(referrerName: string) {
|
|
if (!referrerName) return <FaGlobe className="text-neutral-400" />
|
|
const lower = referrerName.toLowerCase()
|
|
if (lower.includes('google')) return <FaGoogle className="text-blue-500" />
|
|
if (lower.includes('facebook')) return <FaFacebook className="text-blue-600" />
|
|
if (lower.includes('twitter') || lower.includes('t.co') || lower.includes('x.com')) return <FaXTwitter className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('linkedin')) return <FaLinkedin className="text-blue-700" />
|
|
if (lower.includes('instagram')) return <FaInstagram className="text-pink-600" />
|
|
if (lower.includes('github')) return <FaGithub className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('youtube')) return <FaYoutube className="text-red-600" />
|
|
if (lower.includes('reddit')) return <FaReddit className="text-orange-600" />
|
|
// AI assistants and search tools
|
|
if (lower.includes('chatgpt') || lower.includes('openai')) return <SiOpenai className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('perplexity')) return <SiPerplexity className="text-teal-600" />
|
|
if (lower.includes('claude') || lower.includes('anthropic')) return <SiAnthropic className="text-orange-500" />
|
|
if (lower.includes('gemini')) return <SiGooglegemini className="text-blue-500" />
|
|
if (lower.includes('copilot')) return <FaGlobe className="text-blue-500" />
|
|
if (lower.includes('deepseek')) return <RiRobot2Fill className="text-blue-600" />
|
|
if (lower.includes('grok') || lower.includes('x.ai')) return <FaXTwitter className="text-neutral-800 dark:text-neutral-200" />
|
|
if (lower.includes('phind')) return <RiRobot2Fill className="text-purple-600" />
|
|
if (lower.includes('you.com')) return <RiRobot2Fill className="text-indigo-600" />
|
|
|
|
// Try to use a generic globe or maybe check if it is a URL
|
|
return <FaGlobe className="text-neutral-400" />
|
|
}
|
|
|
|
const REFERRER_NO_FAVICON = ['direct', '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<string, string> = {
|
|
chatgpt: 'ChatGPT',
|
|
linkedin: 'LinkedIn',
|
|
youtube: 'YouTube',
|
|
reddit: 'Reddit',
|
|
github: 'GitHub',
|
|
duckduckgo: 'DuckDuckGo',
|
|
whatsapp: 'WhatsApp',
|
|
telegram: 'Telegram',
|
|
pinterest: 'Pinterest',
|
|
snapchat: 'Snapchat',
|
|
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<string, { referrer: string; pageviews: number; maxSingle: number }>()
|
|
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
|
|
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
|
|
}
|
|
}
|