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

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