fix: prevent duplicate filters, support Direct referrer, pass filters to Campaigns

- Deduplicate filters so clicking the same item twice doesn't stack identical pills
- Normalize "Direct" referrer to empty string so direct traffic filtering works
- Pass active filters through to Campaigns component so it respects dashboard filters
This commit is contained in:
Usman Baig
2026-03-06 22:40:57 +01:00
parent ec96fa8a0d
commit 0809c37067
4 changed files with 36 additions and 8 deletions

View File

@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
## [0.13.1-alpha] - 2026-03-06
### Added
- **Hover percentages on dashboard panels.** When you hover over any item in the Content, Locations, Technology, or Top Referrers panels, a percentage now smoothly slides in next to the count — showing you at a glance how much of total traffic that item represents.
- **Click any item to filter your dashboard.** Clicking a referrer, browser, country, city, page, OS, or device in any dashboard panel now instantly filters your entire dashboard to show only that traffic. No need to open the filter modal — just click the item you're curious about.
- **Redesigned filter button.** The old dashed "Add filter" dropdown has been replaced with a clean modal. Pick a dimension from a visual grid, choose an operator, enter a value, and apply — all in a focused overlay.
### Fixed
- **Clicking the same item no longer creates duplicate filters.** Previously, clicking the same referrer or browser multiple times would stack identical filter pills. Now the dashboard recognizes you already have that filter active and ignores the duplicate.
- **"Direct" traffic can now be filtered.** Typing "Direct" as a referrer filter value now correctly matches visitors who arrived without a referrer. Previously this showed zero results because the system didn't recognize "Direct" as a special value.
- **Campaigns now respect active filters.** The Campaigns panel previously ignored your active filters — so if you filtered by a specific referrer or country, campaigns still showed all data. Campaigns now filter along with the rest of your dashboard.
## [0.13.0-alpha] - 2026-03-02
### Added

View File

@@ -98,7 +98,18 @@ export default function SiteDashboardPage() {
const [selectedEvent, setSelectedEvent] = useState<string | null>(null)
const handleAddFilter = useCallback((filter: DimensionFilter) => {
setFilters(prev => [...prev, filter])
// Normalize "Direct" referrer to empty string (direct traffic has no referrer in DB)
const normalized = { ...filter }
if (normalized.dimension === 'referrer') {
normalized.values = normalized.values.map(v => v.toLowerCase() === 'direct' ? '' : v)
}
setFilters(prev => {
const isDuplicate = prev.some(
f => f.dimension === normalized.dimension && f.operator === normalized.operator && f.values.join(';') === normalized.values.join(';')
)
if (isDuplicate) return prev
return [...prev, normalized]
})
}, [])
const handleRemoveFilter = useCallback((index: number) => {
@@ -427,7 +438,7 @@ export default function SiteDashboardPage() {
{/* Campaigns Report */}
<div className="mb-8">
<Campaigns siteId={siteId} dateRange={dateRange} />
<Campaigns siteId={siteId} dateRange={dateRange} filters={filtersParam || undefined} />
</div>
<div className="grid gap-6 lg:grid-cols-2 mb-8">

View File

@@ -17,6 +17,7 @@ import UtmBuilder from '@/components/tools/UtmBuilder'
interface CampaignsProps {
siteId: string
dateRange: { start: string, end: string }
filters?: string
}
const LIMIT = 7
@@ -41,7 +42,7 @@ function campaignRowKey(item: CampaignStat): string {
return `${item.source}|${item.medium}|${item.campaign}`
}
export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
export default function Campaigns({ siteId, dateRange, filters }: CampaignsProps) {
const [data, setData] = useState<CampaignStat[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -56,7 +57,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
const fetchData = async () => {
setIsLoading(true)
try {
const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10)
const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 10, filters)
setData(result)
} catch (e) {
logger.error(e)
@@ -65,14 +66,14 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
}
}
fetchData()
}, [siteId, dateRange])
}, [siteId, dateRange, filters])
useEffect(() => {
if (isModalOpen) {
const fetchFullData = async () => {
setIsLoadingFull(true)
try {
const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100)
const result = await getCampaigns(siteId, dateRange.start, dateRange.end, 100, filters)
setFullData(result)
} catch (e) {
logger.error(e)
@@ -84,7 +85,7 @@ export default function Campaigns({ siteId, dateRange }: CampaignsProps) {
} else {
setFullData([])
}
}, [isModalOpen, siteId, dateRange])
}, [isModalOpen, siteId, dateRange, filters])
const sortedData = useMemo(
() => sortCampaigns(data, sortKey, sortDir),

View File

@@ -55,6 +55,8 @@ export function parseFiltersFromURL(raw: string): DimensionFilter[] {
export function filterLabel(f: DimensionFilter): string {
const dim = DIMENSION_LABELS[f.dimension] || f.dimension
const op = OPERATOR_LABELS[f.operator] || f.operator
const val = f.values.length > 1 ? `${f.values[0]} +${f.values.length - 1}` : f.values[0]
const rawVal = f.values.length > 1 ? `${f.values[0]} +${f.values.length - 1}` : f.values[0]
// Show "Direct" for empty referrer values (direct traffic has no referrer in DB)
const val = f.dimension === 'referrer' && rawVal === '' ? 'Direct' : rawVal
return `${dim} ${op} ${val}`
}