Add search bar to expanded panel modals

This commit is contained in:
Usman Baig
2026-03-10 01:34:05 +01:00
parent a99d13309f
commit 64a8652423
6 changed files with 71 additions and 11 deletions

View File

@@ -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.

View File

@@ -26,6 +26,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
const [data, setData] = useState<CampaignStat[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [modalSearch, setModalSearch] = useState('')
const [isBuilderOpen, setIsBuilderOpen] = useState(false)
const [fullData, setFullData] = useState<CampaignStat[]>([])
const [isLoadingFull, setIsLoadingFull] = useState(false)
@@ -211,17 +212,30 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => { setIsModalOpen(false); setModalSearch('') }}
title="All Campaigns"
className="max-w-2xl"
>
<div>
<input
type="text"
value={modalSearch}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
</div>
) : (() => {
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 (
<>
<div className="flex items-center justify-end mb-2">
@@ -232,7 +246,7 @@ export default function Campaigns({ siteId, dateRange, filters, onFilter }: Camp
Export CSV
</button>
</div>
{sortedFullData.map((item) => (
{filteredCampaigns.map((item) => (
<div
key={`${item.source}|${item.medium}|${item.campaign}`}
onClick={() => { if (onFilter) { onFilter({ dimension: 'utm_source', operator: 'is', values: [item.source] }); setIsModalOpen(false) } }}

View File

@@ -30,6 +30,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
const [activeTab, setActiveTab] = useState<Tab>('top_pages')
const handleTabKeyDown = useTabListKeyboard()
const [isModalOpen, setIsModalOpen] = useState(false)
const [modalSearch, setModalSearch] = useState('')
const [fullData, setFullData] = useState<TopPage[]>([])
const [isLoadingFull, setIsLoadingFull] = useState(false)
@@ -195,17 +196,26 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain,
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => { setIsModalOpen(false); setModalSearch('') }}
title={`Pages - ${getTabLabel(activeTab)}`}
className="max-w-2xl"
>
<div>
<input
type="text"
value={modalSearch}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
</div>
) : (() => {
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

View File

@@ -37,6 +37,7 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
const [activeTab, setActiveTab] = useState<Tab>('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<LocationItem[]>([])
const [isLoadingFull, setIsLoadingFull] = useState(false)
@@ -320,17 +321,31 @@ export default function Locations({ countries, cities, regions, geoDataLevel = '
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => { setIsModalOpen(false); setModalSearch('') }}
title={`Locations - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
className="max-w-2xl"
>
<div>
<input
type="text"
value={modalSearch}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
</div>
) : (() => {
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]

View File

@@ -39,6 +39,7 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
const [activeTab, setActiveTab] = useState<Tab>('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<TechItem[]>([])
const [isLoadingFull, setIsLoadingFull] = useState(false)
@@ -221,17 +222,26 @@ export default function TechSpecs({ browsers, os, devices, screenResolutions, co
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => { setIsModalOpen(false); setModalSearch('') }}
title={`Technology - ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
className="max-w-2xl"
>
<div>
<input
type="text"
value={modalSearch}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
</div>
) : (() => {
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) => {

View File

@@ -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<TopReferrer[]>([])
const [isLoadingFull, setIsLoadingFull] = useState(false)
const [faviconFailed, setFaviconFailed] = useState<Set<string>>(new Set())
@@ -151,17 +152,26 @@ export default function TopReferrers({ referrers, collectReferrers = true, siteI
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onClose={() => { setIsModalOpen(false); setModalSearch('') }}
title="Referrers"
className="max-w-2xl"
>
<div>
<input
type="text"
value={modalSearch}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 max-h-[80vh] overflow-y-auto pr-2">
{isLoadingFull ? (
<div className="py-4">
<ListSkeleton rows={10} />
</div>
) : (() => {
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) => (
<div