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:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user