From 64a865242373c2ddf6a5bfba0ecacc1b2fedf3cd Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Tue, 10 Mar 2026 01:34:05 +0100 Subject: [PATCH] Add search bar to expanded panel modals --- CHANGELOG.md | 1 + components/dashboard/Campaigns.tsx | 20 +++++++++++++++++--- components/dashboard/ContentStats.tsx | 14 ++++++++++++-- components/dashboard/Locations.tsx | 19 +++++++++++++++++-- components/dashboard/TechSpecs.tsx | 14 ++++++++++++-- components/dashboard/TopReferrers.tsx | 14 ++++++++++++-- 6 files changed, 71 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6c53d..9a8c3fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Cleaner focus styles.** Buttons, tabs, and links no longer show an orange outline when you click them — the focus ring now only appears when navigating with the keyboard, keeping the interface clean. - **Faster dashboard loading.** Switching to the Dashboard and Map tabs is now instant — no more brief lag or delay when navigating between sections. - **Expand icon for data panels.** Pages, Referrers, Locations, Technology, and Campaigns panels now show a small expand icon next to the title when there's more data to see, replacing the old "View all" button at the bottom. +- **Better expanded views.** When you expand a data panel, the popup is now wider and taller so you can see more at once. Each row shows a percentage on hover, clicking a row filters your dashboard, and there's a search bar at the top to quickly find what you're looking for. - **Smoother theme switching.** Toggling between light and dark mode now plays a satisfying circular reveal animation that expands from the toggle button, instead of everything just flipping instantly. - **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. diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index ae0894a..87bad2b 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -26,6 +26,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') const [isBuilderOpen, setIsBuilderOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -211,17 +212,30 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp setIsModalOpen(false)} + onClose={() => { setIsModalOpen(false); setModalSearch('') }} title="All Campaigns" className="max-w-2xl" > +
+ setModalSearch(e.target.value)} + placeholder="Search campaigns..." + className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" + /> +
{isLoadingFull ? (
) : (() => { - const modalTotal = sortedFullData.reduce((sum, item) => sum + item.visitors, 0) + const filteredCampaigns = !modalSearch ? sortedFullData : sortedFullData.filter(item => { + const search = modalSearch.toLowerCase() + return item.source.toLowerCase().includes(search) || (item.medium || '').toLowerCase().includes(search) || (item.campaign || '').toLowerCase().includes(search) + }) + const modalTotal = filteredCampaigns.reduce((sum, item) => sum + item.visitors, 0) return ( <>
@@ -232,7 +246,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp Export CSV
- {sortedFullData.map((item) => ( + {filteredCampaigns.map((item) => (
{ if (onFilter) { onFilter({ dimension: 'utm_source', operator: 'is', values: [item.source] }); setIsModalOpen(false) } }} diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index 4295a9d..7ab1989 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -30,6 +30,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, const [activeTab, setActiveTab] = useState('top_pages') const handleTabKeyDown = useTabListKeyboard() const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -195,17 +196,26 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, setIsModalOpen(false)} + onClose={() => { setIsModalOpen(false); setModalSearch('') }} title={`Pages - ${getTabLabel(activeTab)}`} className="max-w-2xl" > +
+ setModalSearch(e.target.value)} + placeholder="Search pages..." + className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" + /> +
{isLoadingFull ? (
) : (() => { - const modalData = fullData.length > 0 ? fullData : data + const modalData = (fullData.length > 0 ? fullData : data).filter(p => !modalSearch || p.path.toLowerCase().includes(modalSearch.toLowerCase())) const modalTotal = modalData.reduce((sum, p) => sum + p.pageviews, 0) return modalData.map((page) => { const canFilter = onFilter && page.path diff --git a/components/dashboard/Locations.tsx b/components/dashboard/Locations.tsx index 717383b..410f039 100644 --- a/components/dashboard/Locations.tsx +++ b/components/dashboard/Locations.tsx @@ -37,6 +37,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = ' const [activeTab, setActiveTab] = useState('map') const handleTabKeyDown = useTabListKeyboard() const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') type LocationItem = { country?: string; city?: string; region?: string; pageviews: number } const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -320,17 +321,31 @@ export default function Locations({ countries, cities, regions, geoDataLevel = ' setIsModalOpen(false)} + onClose={() => { setIsModalOpen(false); setModalSearch('') }} title={`Locations - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} className="max-w-2xl" > +
+ setModalSearch(e.target.value)} + placeholder="Search locations..." + className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" + /> +
{isLoadingFull ? (
) : (() => { - const modalData = fullData.length > 0 ? fullData : data + const rawModalData = fullData.length > 0 ? fullData : data + const search = modalSearch.toLowerCase() + const modalData = !modalSearch ? rawModalData : rawModalData.filter(item => { + const label = activeTab === 'countries' ? getCountryName(item.country ?? '') : activeTab === 'regions' ? getRegionName(item.region ?? '', item.country ?? '') : getCityName(item.city ?? '') + return label.toLowerCase().includes(search) + }) const modalTotal = modalData.reduce((sum, item) => sum + item.pageviews, 0) return modalData.map((item) => { const dim = TAB_TO_DIMENSION[activeTab] diff --git a/components/dashboard/TechSpecs.tsx b/components/dashboard/TechSpecs.tsx index 880548d..d969b4f 100644 --- a/components/dashboard/TechSpecs.tsx +++ b/components/dashboard/TechSpecs.tsx @@ -39,6 +39,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co const [activeTab, setActiveTab] = useState('browsers') const handleTabKeyDown = useTabListKeyboard() const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') type TechItem = { name: string; pageviews: number; icon: React.ReactNode } const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -221,17 +222,26 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co setIsModalOpen(false)} + onClose={() => { setIsModalOpen(false); setModalSearch('') }} title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`} className="max-w-2xl" > +
+ setModalSearch(e.target.value)} + placeholder="Search technology..." + className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" + /> +
{isLoadingFull ? (
) : (() => { - const modalData = fullData.length > 0 ? fullData : data + const modalData = (fullData.length > 0 ? fullData : data).filter(item => !modalSearch || item.name.toLowerCase().includes(modalSearch.toLowerCase())) const modalTotal = modalData.reduce((sum, item) => sum + item.pageviews, 0) const dim = TAB_TO_DIMENSION[activeTab] return modalData.map((item) => { diff --git a/components/dashboard/TopReferrers.tsx b/components/dashboard/TopReferrers.tsx index 15918ef..c5df9e4 100644 --- a/components/dashboard/TopReferrers.tsx +++ b/components/dashboard/TopReferrers.tsx @@ -23,6 +23,7 @@ const LIMIT = 7 export default function TopReferrers({ referrers, collectReferrers = true, siteId, dateRange, onFilter }: TopReferrersProps) { const [isModalOpen, setIsModalOpen] = useState(false) + const [modalSearch, setModalSearch] = useState('') const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) const [faviconFailed, setFaviconFailed] = useState>(new Set()) @@ -151,17 +152,26 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI setIsModalOpen(false)} + onClose={() => { setIsModalOpen(false); setModalSearch('') }} title="Referrers" className="max-w-2xl" > +
+ setModalSearch(e.target.value)} + placeholder="Search referrers..." + className="w-full px-3 py-2 mb-3 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-brand-orange/50" + /> +
{isLoadingFull ? (
) : (() => { - const modalData = mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers) + const modalData = mergeReferrersByDisplayName(fullData.length > 0 ? fullData : filteredReferrers).filter(r => !modalSearch || getReferrerDisplayName(r.referrer).toLowerCase().includes(modalSearch.toLowerCase())) const modalTotal = modalData.reduce((sum, r) => sum + r.pageviews, 0) return modalData.map((ref) => (