feat: replace filter modal with chip-based dimension filter bar
Dimension chips (Page, Referrer, Country, Browser, OS, Device + More) with popover dropdowns showing real values from current dashboard data with counts. Operators inline, search/type custom values, click to apply.
This commit is contained in:
@@ -22,7 +22,7 @@ import GoalStats from '@/components/dashboard/GoalStats'
|
||||
import ScrollDepth from '@/components/dashboard/ScrollDepth'
|
||||
import Campaigns from '@/components/dashboard/Campaigns'
|
||||
import FilterBar from '@/components/dashboard/FilterBar'
|
||||
import AddFilterDropdown from '@/components/dashboard/AddFilterDropdown'
|
||||
import AddFilterDropdown, { type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
|
||||
import EventProperties from '@/components/dashboard/EventProperties'
|
||||
import { type DimensionFilter, serializeFilters, parseFiltersFromURL } from '@/lib/filters'
|
||||
import {
|
||||
@@ -168,6 +168,109 @@ export default function SiteDashboardPage() {
|
||||
const realtime = realtimeData?.visitors ?? overview?.realtime_visitors ?? 0
|
||||
const dailyStats: DailyStat[] = overview?.daily_stats ?? []
|
||||
|
||||
// Build filter suggestions from current dashboard data
|
||||
const filterSuggestions = useMemo<FilterSuggestions>(() => {
|
||||
const s: FilterSuggestions = {}
|
||||
|
||||
// Pages
|
||||
const topPages = pages?.top_pages ?? []
|
||||
if (topPages.length > 0) {
|
||||
s.page = topPages.map(p => ({ value: p.path, label: p.path, count: p.pageviews }))
|
||||
}
|
||||
|
||||
// Referrers
|
||||
const refs = referrers?.top_referrers ?? []
|
||||
if (refs.length > 0) {
|
||||
s.referrer = [
|
||||
{ value: '', label: 'Direct', count: undefined },
|
||||
...refs.filter(r => r.referrer && r.referrer !== '').map(r => ({
|
||||
value: r.referrer,
|
||||
label: r.referrer,
|
||||
count: r.pageviews,
|
||||
})),
|
||||
]
|
||||
}
|
||||
|
||||
// Countries
|
||||
const ctrs = locations?.countries ?? []
|
||||
if (ctrs.length > 0) {
|
||||
const regionNames = (() => { try { return new Intl.DisplayNames(['en'], { type: 'region' }) } catch { return null } })()
|
||||
s.country = ctrs.filter(c => c.country && c.country !== 'Unknown').map(c => ({
|
||||
value: c.country,
|
||||
label: regionNames?.of(c.country) ?? c.country,
|
||||
count: c.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// Regions
|
||||
const regs = locations?.regions ?? []
|
||||
if (regs.length > 0) {
|
||||
s.region = regs.filter(r => r.region && r.region !== 'Unknown').map(r => ({
|
||||
value: r.region,
|
||||
label: r.region,
|
||||
count: r.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// Cities
|
||||
const cts = locations?.cities ?? []
|
||||
if (cts.length > 0) {
|
||||
s.city = cts.filter(c => c.city && c.city !== 'Unknown').map(c => ({
|
||||
value: c.city,
|
||||
label: c.city,
|
||||
count: c.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// Browsers
|
||||
const brs = devicesData?.browsers ?? []
|
||||
if (brs.length > 0) {
|
||||
s.browser = brs.filter(b => b.browser && b.browser !== 'Unknown').map(b => ({
|
||||
value: b.browser,
|
||||
label: b.browser,
|
||||
count: b.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// OS
|
||||
const oses = devicesData?.os ?? []
|
||||
if (oses.length > 0) {
|
||||
s.os = oses.filter(o => o.os && o.os !== 'Unknown').map(o => ({
|
||||
value: o.os,
|
||||
label: o.os,
|
||||
count: o.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// Devices
|
||||
const devs = devicesData?.devices ?? []
|
||||
if (devs.length > 0) {
|
||||
s.device = devs.filter(d => d.device && d.device !== 'Unknown').map(d => ({
|
||||
value: d.device,
|
||||
label: d.device,
|
||||
count: d.pageviews,
|
||||
}))
|
||||
}
|
||||
|
||||
// UTM from campaigns
|
||||
const camps = campaigns ?? []
|
||||
if (camps.length > 0) {
|
||||
const sources = new Map<string, number>()
|
||||
const mediums = new Map<string, number>()
|
||||
const campNames = new Map<string, number>()
|
||||
camps.forEach(c => {
|
||||
if (c.source) sources.set(c.source, (sources.get(c.source) ?? 0) + c.pageviews)
|
||||
if (c.medium) mediums.set(c.medium, (mediums.get(c.medium) ?? 0) + c.pageviews)
|
||||
if (c.campaign) campNames.set(c.campaign, (campNames.get(c.campaign) ?? 0) + c.pageviews)
|
||||
})
|
||||
if (sources.size > 0) s.utm_source = [...sources.entries()].map(([v, c]) => ({ value: v, label: v, count: c }))
|
||||
if (mediums.size > 0) s.utm_medium = [...mediums.entries()].map(([v, c]) => ({ value: v, label: v, count: c }))
|
||||
if (campNames.size > 0) s.utm_campaign = [...campNames.entries()].map(([v, c]) => ({ value: v, label: v, count: c }))
|
||||
}
|
||||
|
||||
return s
|
||||
}, [pages, referrers, locations, devicesData, campaigns])
|
||||
|
||||
// Show error toast on fetch failure
|
||||
useEffect(() => {
|
||||
if (overviewError) {
|
||||
@@ -357,9 +460,9 @@ export default function SiteDashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Dimension Filters */}
|
||||
<div className="flex items-center gap-2 flex-wrap mb-2">
|
||||
<div className="space-y-2 mb-2">
|
||||
<AddFilterDropdown onAdd={handleAddFilter} suggestions={filterSuggestions} />
|
||||
<FilterBar filters={filters} onRemove={handleRemoveFilter} onClear={handleClearFilters} />
|
||||
<AddFilterDropdown onAdd={handleAddFilter} />
|
||||
</div>
|
||||
|
||||
{/* Advanced Chart with Integrated Stats */}
|
||||
|
||||
Reference in New Issue
Block a user