diff --git a/CHANGELOG.md b/CHANGELOG.md index 45fb3b8..1b98cb8 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 4646267..24a18e1 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -98,7 +98,18 @@ export default function SiteDashboardPage() { const [selectedEvent, setSelectedEvent] = useState(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 */}
- +
diff --git a/components/dashboard/Campaigns.tsx b/components/dashboard/Campaigns.tsx index 31bed08..f8f49ab 100644 --- a/components/dashboard/Campaigns.tsx +++ b/components/dashboard/Campaigns.tsx @@ -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([]) 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), diff --git a/lib/filters.ts b/lib/filters.ts index e6fbac2..608083a 100644 --- a/lib/filters.ts +++ b/lib/filters.ts @@ -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}` }