refactor: switch icons from react-icons to Phosphor

Replace react-icons and @radix-ui/react-icons with @phosphor-icons/react
for a consistent icon style across all dashboard panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Usman Baig
2026-03-09 00:23:31 +01:00
parent 397a5afef9
commit 7f9ad0e977
10 changed files with 84 additions and 100 deletions

View File

@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Improved
- **Cleaner site navigation.** Dashboard, Uptime, Funnels, and Settings now use an underline tab bar instead of floating buttons. The active section is highlighted with an orange underline, making it easy to see where you are and switch between views.
- **Consistent icon style.** All dashboard icons now use a single, unified icon set for a cleaner look across Technology, Locations, Campaigns, and Referrers panels.
### Fixed

View File

@@ -1,13 +1,13 @@
'use client';
import { FiWifiOff } from 'react-icons/fi';
import { WifiSlash } from '@phosphor-icons/react';
export function OfflineBanner({ isOnline }: { isOnline: boolean }) {
if (isOnline) return null;
return (
<div className="fixed top-0 left-0 right-0 z-[100] rounded-b-xl bg-yellow-500/15 dark:bg-yellow-500/25 border-b border-yellow-500/30 dark:border-yellow-500/40 text-yellow-700 dark:text-yellow-300 px-4 sm:px-8 py-2.5 text-sm flex items-center justify-center gap-2 font-medium shadow-md transition-shadow duration-300">
<FiWifiOff className="w-4 h-4 shrink-0" />
<WifiSlash className="w-4 h-4 shrink-0" />
<span>You are currently offline. Changes may not be saved.</span>
</div>
);

View File

@@ -9,7 +9,7 @@ import { Modal, ArrowRightIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons'
import { getCampaigns, CampaignStat } from '@/lib/api/stats'
import { getReferrerFavicon, getReferrerIcon, getReferrerDisplayName } from '@/lib/utils/icons'
import { FaBullhorn } from 'react-icons/fa'
import { Megaphone } from '@phosphor-icons/react'
import UtmBuilder from '@/components/tools/UtmBuilder'
import { type DimensionFilter } from '@/lib/filters'
@@ -190,7 +190,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
) : (
<div className="h-full flex flex-col items-center justify-center text-center px-6 py-8 gap-3">
<div className="rounded-full bg-neutral-100 dark:bg-neutral-800 p-4">
<FaBullhorn className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
<Megaphone className="w-8 h-8 text-neutral-500 dark:text-neutral-400" />
</div>
<h4 className="font-semibold text-neutral-900 dark:text-white">
Track your marketing campaigns

View File

@@ -9,8 +9,7 @@ import iso3166 from 'iso-3166-2'
import WorldMap from './WorldMap'
import { Modal, GlobeIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons'
import { SiTorproject } from 'react-icons/si'
import { FaUserSecret, FaSatellite } from 'react-icons/fa'
import { ShieldCheck, Detective, Broadcast } from '@phosphor-icons/react'
import { getCountries, getCities, getRegions } from '@/lib/api/stats'
import { type DimensionFilter } from '@/lib/filters'
@@ -69,11 +68,11 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
switch (countryCode) {
case 'T1':
return <SiTorproject className="w-5 h-5 text-purple-600 dark:text-purple-400" />
return <ShieldCheck className="w-5 h-5 text-purple-600 dark:text-purple-400" />
case 'A1':
return <FaUserSecret className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
return <Detective className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
case 'A2':
return <FaSatellite className="w-5 h-5 text-blue-500 dark:text-blue-400" />
return <Broadcast className="w-5 h-5 text-blue-500 dark:text-blue-400" />
case 'O1':
case 'EU':
case 'AP':

View File

@@ -5,7 +5,7 @@ import { logger } from '@/lib/utils/logger'
import { formatNumber } from '@ciphera-net/ui'
import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard'
import { getBrowserIcon, getOSIcon, getDeviceIcon } from '@/lib/utils/icons'
import { MdMonitor } from 'react-icons/md'
import { Monitor } from '@phosphor-icons/react'
import { Modal, GridIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons'
import { getBrowsers, getOS, getDevices, getScreenResolutions } from '@/lib/api/stats'
@@ -64,7 +64,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
data = res.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) }))
} else if (activeTab === 'screens') {
const res = await getScreenResolutions(siteId, dateRange.start, dateRange.end, 100)
data = res.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: <MdMonitor className="text-neutral-500" /> }))
data = res.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: <Monitor className="text-neutral-500" /> }))
}
setFullData(filterUnknown(data))
} catch (e) {
@@ -88,7 +88,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
case 'devices':
return devices.map(d => ({ name: d.device, pageviews: d.pageviews, icon: getDeviceIcon(d.device) }))
case 'screens':
return screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: <MdMonitor className="text-neutral-500" /> }))
return screenResolutions.map(s => ({ name: s.screen_resolution, pageviews: s.pageviews, icon: <Monitor className="text-neutral-500" /> }))
default:
return []
}

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger'
import { CopyIcon, CheckIcon } from '@radix-ui/react-icons'
import { Copy, Check } from '@phosphor-icons/react'
import { listSites, Site } from '@/lib/api/sites'
import { Select, Input, Button } from '@ciphera-net/ui'
@@ -205,7 +205,7 @@ export default function UtmBuilder({ initialSiteId }: UtmBuilderProps) {
className="ml-4 shrink-0 h-9 w-9 p-0 rounded-lg"
title="Copy to clipboard"
>
{copied ? <CheckIcon className="w-4 h-4 text-green-500" /> : <CopyIcon className="w-4 h-4" />}
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
</Button>
</div>
)}

View File

@@ -1,98 +1,80 @@
import React from 'react'
import {
Globe,
WindowsLogo,
AppleLogo,
LinuxLogo,
AndroidLogo,
Question,
DeviceMobile,
DeviceTablet,
Desktop,
GoogleLogo,
FacebookLogo,
XLogo,
LinkedinLogo,
InstagramLogo,
GithubLogo,
YoutubeLogo,
RedditLogo,
Robot,
} from '@phosphor-icons/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" />
if (!browserName) return <Globe className="text-neutral-400" />
return <Globe className="text-neutral-500" />
}
export function getOSIcon(osName: string) {
if (!osName) return <MdDeviceUnknown className="text-neutral-400" />
if (!osName) return <Question 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" />
if (lower.includes('win')) return <WindowsLogo className="text-blue-500" />
if (lower.includes('mac') || lower.includes('ios')) return <AppleLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('linux') || lower.includes('ubuntu') || lower.includes('debian')) return <LinuxLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('android')) return <AndroidLogo className="text-green-500" />
return <Question className="text-neutral-400" />
}
export function getDeviceIcon(deviceName: string) {
if (!deviceName) return <MdDeviceUnknown className="text-neutral-400" />
if (!deviceName) return <Question 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" />
if (lower.includes('mobile') || lower.includes('phone')) return <DeviceMobile className="text-neutral-500" />
if (lower.includes('tablet') || lower.includes('ipad')) return <DeviceTablet className="text-neutral-500" />
if (lower.includes('desktop') || lower.includes('laptop')) return <Desktop className="text-neutral-500" />
return <Question className="text-neutral-400" />
}
export function getReferrerIcon(referrerName: string) {
if (!referrerName) return <FaGlobe className="text-neutral-400" />
if (!referrerName) return <Globe 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" />
if (lower.includes('google')) return <GoogleLogo className="text-blue-500" />
if (lower.includes('facebook')) return <FacebookLogo className="text-blue-600" />
if (lower.includes('twitter') || lower.includes('t.co') || lower.includes('x.com')) return <XLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('linkedin')) return <LinkedinLogo className="text-blue-700" />
if (lower.includes('instagram')) return <InstagramLogo className="text-pink-600" />
if (lower.includes('github')) return <GithubLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('youtube')) return <YoutubeLogo className="text-red-600" />
if (lower.includes('reddit')) return <RedditLogo 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" />
if (lower.includes('chatgpt') || lower.includes('openai')) return <Robot className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('perplexity')) return <Robot className="text-teal-600" />
if (lower.includes('claude') || lower.includes('anthropic')) return <Robot className="text-orange-500" />
if (lower.includes('gemini')) return <Robot className="text-blue-500" />
if (lower.includes('copilot')) return <Robot className="text-blue-500" />
if (lower.includes('deepseek')) return <Robot className="text-blue-600" />
if (lower.includes('grok') || lower.includes('x.ai')) return <XLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('phind')) return <Robot className="text-purple-600" />
if (lower.includes('you.com')) return <Robot className="text-indigo-600" />
// Try to use a generic globe or maybe check if it is a URL
return <FaGlobe className="text-neutral-400" />
return <Globe className="text-neutral-400" />
}
const REFERRER_NO_FAVICON = ['direct', 'unknown', '']

View File

@@ -30,7 +30,7 @@ const nextConfig: NextConfig = {
// * Privacy-first: Disable analytics and telemetry
productionBrowserSourceMaps: false,
experimental: {
optimizePackageImports: ['react-icons'],
optimizePackageImports: ['@phosphor-icons/react'],
},
images: {
remotePatterns: [

25
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"dependencies": {
"@ciphera-net/ui": "^0.0.93",
"@ducanh2912/next-pwa": "^10.2.9",
"@radix-ui/react-icons": "^1.3.0",
"@phosphor-icons/react": "^2.1.10",
"@simplewebauthn/browser": "^13.2.2",
"@stripe/react-stripe-js": "^5.6.0",
"@stripe/stripe-js": "^8.7.0",
@@ -26,7 +26,6 @@
"next": "^16.1.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",
@@ -3274,6 +3273,19 @@
"node": ">=12.4.0"
}
},
"node_modules/@phosphor-icons/react": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">= 16.8",
"react-dom": ">= 16.8"
}
},
"node_modules/@radix-ui/react-icons": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
@@ -11081,15 +11093,6 @@
"react": "^19.2.4"
}
},
"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

@@ -14,7 +14,7 @@
"dependencies": {
"@ciphera-net/ui": "^0.0.93",
"@ducanh2912/next-pwa": "^10.2.9",
"@radix-ui/react-icons": "^1.3.0",
"@phosphor-icons/react": "^2.1.10",
"@simplewebauthn/browser": "^13.2.2",
"@stripe/react-stripe-js": "^5.6.0",
"@stripe/stripe-js": "^8.7.0",
@@ -30,7 +30,6 @@
"next": "^16.1.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-simple-maps": "^3.0.0",
"recharts": "^2.15.0",