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 ### 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. - **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 ### Fixed

View File

@@ -1,13 +1,13 @@
'use client'; 'use client';
import { FiWifiOff } from 'react-icons/fi'; import { WifiSlash } from '@phosphor-icons/react';
export function OfflineBanner({ isOnline }: { isOnline: boolean }) { export function OfflineBanner({ isOnline }: { isOnline: boolean }) {
if (isOnline) return null; if (isOnline) return null;
return ( 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"> <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> <span>You are currently offline. Changes may not be saved.</span>
</div> </div>
); );

View File

@@ -9,7 +9,7 @@ import { Modal, ArrowRightIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons' import { ListSkeleton } from '@/components/skeletons'
import { getCampaigns, CampaignStat } from '@/lib/api/stats' import { getCampaigns, CampaignStat } from '@/lib/api/stats'
import { getReferrerFavicon, getReferrerIcon, getReferrerDisplayName } from '@/lib/utils/icons' 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 UtmBuilder from '@/components/tools/UtmBuilder'
import { type DimensionFilter } from '@/lib/filters' 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="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"> <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> </div>
<h4 className="font-semibold text-neutral-900 dark:text-white"> <h4 className="font-semibold text-neutral-900 dark:text-white">
Track your marketing campaigns Track your marketing campaigns

View File

@@ -9,8 +9,7 @@ import iso3166 from 'iso-3166-2'
import WorldMap from './WorldMap' import WorldMap from './WorldMap'
import { Modal, GlobeIcon } from '@ciphera-net/ui' import { Modal, GlobeIcon } from '@ciphera-net/ui'
import { ListSkeleton } from '@/components/skeletons' import { ListSkeleton } from '@/components/skeletons'
import { SiTorproject } from 'react-icons/si' import { ShieldCheck, Detective, Broadcast } from '@phosphor-icons/react'
import { FaUserSecret, FaSatellite } from 'react-icons/fa'
import { getCountries, getCities, getRegions } from '@/lib/api/stats' import { getCountries, getCities, getRegions } from '@/lib/api/stats'
import { type DimensionFilter } from '@/lib/filters' import { type DimensionFilter } from '@/lib/filters'
@@ -69,11 +68,11 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
switch (countryCode) { switch (countryCode) {
case 'T1': 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': 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': 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 'O1':
case 'EU': case 'EU':
case 'AP': case 'AP':

View File

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

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { logger } from '@/lib/utils/logger' 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 { listSites, Site } from '@/lib/api/sites'
import { Select, Input, Button } from '@ciphera-net/ui' 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" className="ml-4 shrink-0 h-9 w-9 p-0 rounded-lg"
title="Copy to clipboard" 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> </Button>
</div> </div>
)} )}

View File

@@ -1,98 +1,80 @@
import React from 'react' 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. * Google's public favicon service base URL.
* Append `?domain=<host>&sz=<px>` to get a favicon. * Append `?domain=<host>&sz=<px>` to get a favicon.
*/ */
export const FAVICON_SERVICE_URL = 'https://www.google.com/s2/favicons' 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) { export function getBrowserIcon(browserName: string) {
if (!browserName) return <FaGlobe className="text-neutral-400" /> if (!browserName) return <Globe className="text-neutral-400" />
const lower = browserName.toLowerCase() return <Globe className="text-neutral-500" />
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) { 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() const lower = osName.toLowerCase()
if (lower.includes('win')) return <FaWindows className="text-blue-500" /> if (lower.includes('win')) return <WindowsLogo className="text-blue-500" />
if (lower.includes('mac') || lower.includes('ios')) return <FaApple className="text-neutral-800 dark:text-neutral-200" /> 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 <FaLinux 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 <FaAndroid className="text-green-500" /> if (lower.includes('android')) return <AndroidLogo className="text-green-500" />
return <MdDeviceUnknown className="text-neutral-400" /> return <Question className="text-neutral-400" />
} }
export function getDeviceIcon(deviceName: string) { 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() const lower = deviceName.toLowerCase()
if (lower.includes('mobile') || lower.includes('phone')) return <MdSmartphone className="text-neutral-500" /> if (lower.includes('mobile') || lower.includes('phone')) return <DeviceMobile className="text-neutral-500" />
if (lower.includes('tablet') || lower.includes('ipad')) return <MdTabletMac 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 <MdDesktopWindows className="text-neutral-500" /> if (lower.includes('desktop') || lower.includes('laptop')) return <Desktop className="text-neutral-500" />
return <MdDeviceUnknown className="text-neutral-400" /> return <Question className="text-neutral-400" />
} }
export function getReferrerIcon(referrerName: string) { 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() const lower = referrerName.toLowerCase()
if (lower.includes('google')) return <FaGoogle className="text-blue-500" /> if (lower.includes('google')) return <GoogleLogo className="text-blue-500" />
if (lower.includes('facebook')) return <FaFacebook className="text-blue-600" /> if (lower.includes('facebook')) return <FacebookLogo 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('twitter') || lower.includes('t.co') || lower.includes('x.com')) return <XLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('linkedin')) return <FaLinkedin className="text-blue-700" /> if (lower.includes('linkedin')) return <LinkedinLogo className="text-blue-700" />
if (lower.includes('instagram')) return <FaInstagram className="text-pink-600" /> if (lower.includes('instagram')) return <InstagramLogo className="text-pink-600" />
if (lower.includes('github')) return <FaGithub className="text-neutral-800 dark:text-neutral-200" /> if (lower.includes('github')) return <GithubLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('youtube')) return <FaYoutube className="text-red-600" /> if (lower.includes('youtube')) return <YoutubeLogo className="text-red-600" />
if (lower.includes('reddit')) return <FaReddit className="text-orange-600" /> if (lower.includes('reddit')) return <RedditLogo className="text-orange-600" />
// AI assistants and search tools // 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('chatgpt') || lower.includes('openai')) return <Robot className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('perplexity')) return <SiPerplexity className="text-teal-600" /> if (lower.includes('perplexity')) return <Robot className="text-teal-600" />
if (lower.includes('claude') || lower.includes('anthropic')) return <SiAnthropic className="text-orange-500" /> if (lower.includes('claude') || lower.includes('anthropic')) return <Robot className="text-orange-500" />
if (lower.includes('gemini')) return <SiGooglegemini className="text-blue-500" /> if (lower.includes('gemini')) return <Robot className="text-blue-500" />
if (lower.includes('copilot')) return <FaGlobe className="text-blue-500" /> if (lower.includes('copilot')) return <Robot className="text-blue-500" />
if (lower.includes('deepseek')) return <RiRobot2Fill className="text-blue-600" /> if (lower.includes('deepseek')) return <Robot 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('grok') || lower.includes('x.ai')) return <XLogo className="text-neutral-800 dark:text-neutral-200" />
if (lower.includes('phind')) return <RiRobot2Fill className="text-purple-600" /> if (lower.includes('phind')) return <Robot className="text-purple-600" />
if (lower.includes('you.com')) return <RiRobot2Fill className="text-indigo-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 <Globe className="text-neutral-400" />
return <FaGlobe className="text-neutral-400" />
} }
const REFERRER_NO_FAVICON = ['direct', 'unknown', ''] const REFERRER_NO_FAVICON = ['direct', 'unknown', '']

View File

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

25
package-lock.json generated
View File

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

View File

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