feat(analytics): add icons for browsers, devices, OS, and referrers

This commit is contained in:
Usman Baig
2026-01-16 23:39:10 +01:00
parent e1cf7d4b13
commit 42492d64b9
5 changed files with 113 additions and 9 deletions

View File

@@ -2,6 +2,8 @@
import { useState } from 'react'
import { formatNumber } from '@/lib/utils/format'
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
import { MdMonitor } from 'react-icons/md'
interface TechSpecsProps {
browsers: Array<{ browser: string; pageviews: number }>
@@ -16,16 +18,16 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions }:
const [activeTab, setActiveTab] = useState<Tab>('browsers')
const renderContent = () => {
let data: Array<{ name: string; pageviews: number }> = []
let data: Array<{ name: string; pageviews: number; icon?: React.ReactNode }> = []
if (activeTab === 'browsers') {
data = browsers.map(b => ({ name: b.browser, pageviews: b.pageviews }))
data = browsers.map(b => ({ name: b.browser, pageviews: b.pageviews, icon: getBrowserIcon(b.browser) }))
} else if (activeTab === 'os') {
data = os.map(o => ({ name: o.os, pageviews: o.pageviews }))
data = os.map(o => ({ name: o.os, pageviews: o.pageviews, icon: getOSIcon(o.os) }))
} else if (activeTab === 'devices') {
data = devices.map(d => ({ name: d.device, pageviews: d.pageviews }))
data = devices.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) }))
} else if (activeTab === 'screens') {
data = screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews }))
data = screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: <MdMonitor className="text-neutral-500" /> }))
}
if (!data || data.length === 0) {
@@ -37,6 +39,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions }:
{data.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
{item.icon && <span className="text-lg">{item.icon}</span>}
<span className="truncate">{item.name === 'Unknown' ? 'Unknown' : item.name}</span>
</div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">

View File

@@ -1,6 +1,7 @@
'use client'
import { formatNumber } from '@/lib/utils/format'
import { getReferrerIcon } from '@/lib/utils/icons'
interface TopReferrersProps {
referrers: Array<{ referrer: string; pageviews: number }>
@@ -9,7 +10,7 @@ interface TopReferrersProps {
export default function TopReferrers({ referrers }: TopReferrersProps) {
if (!referrers || referrers.length === 0) {
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full">
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Referrers
</h3>
@@ -19,15 +20,16 @@ export default function TopReferrers({ referrers }: TopReferrersProps) {
}
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6">
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl p-6 h-full">
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
Top Referrers
</h3>
<div className="space-y-3">
{referrers.map((ref, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1 truncate text-neutral-900 dark:text-white">
{ref.referrer}
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
<span className="text-lg flex-shrink-0">{getReferrerIcon(ref.referrer)}</span>
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
</div>
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
{formatNumber(ref.pageviews)}

88
lib/utils/icons.tsx Normal file
View File

@@ -0,0 +1,88 @@
import React from 'react'
import {
FaChrome,
FaFirefox,
FaSafari,
FaEdge,
FaOpera,
FaInternetExplorer,
FaWindows,
FaApple,
FaLinux,
FaAndroid,
FaDesktop,
FaMobileAlt,
FaTabletAlt,
FaGoogle,
FaFacebook,
FaTwitter,
FaLinkedin,
FaInstagram,
FaGithub,
FaYoutube,
FaReddit,
FaQuestion,
FaGlobe
} from 'react-icons/fa'
import { SiBrave } from 'react-icons/si'
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 <FaTwitter className="text-blue-400" />
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" />
// Try to use a generic globe or maybe check if it is a URL
return <FaGlobe className="text-neutral-400" />
}
export function getReferrerFavicon(referrer: string) {
try {
const url = new URL(referrer.startsWith('http') ? referrer : `https://${referrer}`);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch (e) {
return null;
}
}

10
package-lock.json generated
View File

@@ -19,6 +19,7 @@
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
"sonner": "^2.0.7"
@@ -5910,6 +5911,15 @@
"react": "^19.2.3"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -21,6 +21,7 @@
"next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
"sonner": "^2.0.7"