Add search bar to expanded panel modals
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) } }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user