feat(analytics): add icons for browsers, devices, OS, and referrers
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { formatNumber } from '@/lib/utils/format'
|
import { formatNumber } from '@/lib/utils/format'
|
||||||
|
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
|
||||||
|
import { MdMonitor } from 'react-icons/md'
|
||||||
|
|
||||||
interface TechSpecsProps {
|
interface TechSpecsProps {
|
||||||
browsers: Array<{ browser: string; pageviews: number }>
|
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 [activeTab, setActiveTab] = useState<Tab>('browsers')
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
let data: Array<{ name: string; pageviews: number }> = []
|
let data: Array<{ name: string; pageviews: number; icon?: React.ReactNode }> = []
|
||||||
|
|
||||||
if (activeTab === 'browsers') {
|
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') {
|
} 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') {
|
} 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') {
|
} 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) {
|
if (!data || data.length === 0) {
|
||||||
@@ -37,6 +39,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions }:
|
|||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
<div key={index} className="flex items-center justify-between">
|
<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">
|
<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>
|
<span className="truncate">{item.name === 'Unknown' ? 'Unknown' : item.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { formatNumber } from '@/lib/utils/format'
|
import { formatNumber } from '@/lib/utils/format'
|
||||||
|
import { getReferrerIcon } from '@/lib/utils/icons'
|
||||||
|
|
||||||
interface TopReferrersProps {
|
interface TopReferrersProps {
|
||||||
referrers: Array<{ referrer: string; pageviews: number }>
|
referrers: Array<{ referrer: string; pageviews: number }>
|
||||||
@@ -9,7 +10,7 @@ interface TopReferrersProps {
|
|||||||
export default function TopReferrers({ referrers }: TopReferrersProps) {
|
export default function TopReferrers({ referrers }: TopReferrersProps) {
|
||||||
if (!referrers || referrers.length === 0) {
|
if (!referrers || referrers.length === 0) {
|
||||||
return (
|
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">
|
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
|
||||||
Top Referrers
|
Top Referrers
|
||||||
</h3>
|
</h3>
|
||||||
@@ -19,15 +20,16 @@ export default function TopReferrers({ referrers }: TopReferrersProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<h3 className="text-lg font-semibold mb-4 text-neutral-900 dark:text-white">
|
||||||
Top Referrers
|
Top Referrers
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{referrers.map((ref, index) => (
|
{referrers.map((ref, index) => (
|
||||||
<div key={index} className="flex items-center justify-between">
|
<div key={index} className="flex items-center justify-between">
|
||||||
<div className="flex-1 truncate text-neutral-900 dark:text-white">
|
<div className="flex-1 truncate text-neutral-900 dark:text-white flex items-center gap-3">
|
||||||
{ref.referrer}
|
<span className="text-lg flex-shrink-0">{getReferrerIcon(ref.referrer)}</span>
|
||||||
|
<span className="truncate" title={ref.referrer}>{ref.referrer}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
<div className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 ml-4">
|
||||||
{formatNumber(ref.pageviews)}
|
{formatNumber(ref.pageviews)}
|
||||||
|
|||||||
88
lib/utils/icons.tsx
Normal file
88
lib/utils/icons.tsx
Normal 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
10
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-simple-maps": "^3.0.0",
|
"react-simple-maps": "^3.0.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
@@ -5910,6 +5911,15 @@
|
|||||||
"react": "^19.2.3"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.3",
|
"react": "^19.2.3",
|
||||||
"react-dom": "^19.2.3",
|
"react-dom": "^19.2.3",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-simple-maps": "^3.0.0",
|
"react-simple-maps": "^3.0.0",
|
||||||
"recharts": "^2.15.0",
|
"recharts": "^2.15.0",
|
||||||
"sonner": "^2.0.7"
|
"sonner": "^2.0.7"
|
||||||
|
|||||||
Reference in New Issue
Block a user