feat: polish Goals & Events dashboard panel

Align visual style with Pages, Referrers, and Locations panels — flat
rows with hover state, rank numbers, muted count colors, and a
slide-in percentage on hover. Add empty slots for consistent height.
This commit is contained in:
Usman Baig
2026-03-11 22:15:59 +01:00
parent c2d5935394
commit 2fa3540a48
2 changed files with 25 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
### Improved
- **Polished Goals & Events panel.** The Goals & Events block on your dashboard got a visual refresh to match the style of the Pages, Referrers, and Locations panels. Events are now ranked with a number on the left, counts are shown in a consistent style, and hovering any row reveals what percentage of total events that action accounts for — sliding in smoothly from the right.
- **Smarter bot protection.** The security checks on shared dashboard access and organization settings now use action-specific tokens tied to each page. A token earned on one page can't be reused on another, making it harder for automated tools to bypass the captcha.
- **More resilient under Redis outages.** If the caching layer goes down temporarily, Pulse now continues enforcing rate limits using an in-memory fallback instead of letting all traffic through unchecked. This prevents one infrastructure hiccup from snowballing into a bigger problem.
- **Better handling of traffic bursts.** The system can now absorb 5x larger spikes of incoming events before applying backpressure. When events are dropped during extreme bursts, the system now tracks and logs exactly how many — so we can detect and respond to sustained overload before it affects your data.

View File

@@ -15,6 +15,8 @@ const LIMIT = 10
export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps) {
const list = (goalCounts || []).slice(0, LIMIT)
const hasData = list.length > 0
const total = list.reduce((sum, r) => sum + r.count, 0)
const emptySlots = Math.max(0, 6 - list.length)
return (
<div className="bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl p-6 h-full flex flex-col">
@@ -25,20 +27,33 @@ export default function GoalStats({ goalCounts, onSelectEvent }: GoalStatsProps)
</div>
{hasData ? (
<div className="space-y-2 flex-1 min-h-[200px]">
{list.map((row) => (
<div className="flex-1 min-h-[200px]">
{list.map((row, i) => (
<div
key={row.event_name}
onClick={() => onSelectEvent?.(row.event_name)}
className={`flex items-center justify-between py-2 px-3 rounded-lg bg-neutral-50 dark:bg-neutral-800/50 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors${onSelectEvent ? ' cursor-pointer' : ''}`}
className={`flex items-center justify-between h-9 group hover:bg-neutral-50 dark:hover:bg-neutral-800 rounded-lg px-2 -mx-2 transition-colors${onSelectEvent ? ' cursor-pointer' : ''}`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<span className="text-xs font-medium text-neutral-400 dark:text-neutral-600 w-4 text-right flex-shrink-0 tabular-nums">
{i + 1}
</span>
<span className="text-sm font-medium text-neutral-900 dark:text-white truncate">
{row.display_name ?? row.event_name.replace(/_/g, ' ')}
</span>
<span className="text-sm font-semibold text-brand-orange tabular-nums">
</div>
<div className="flex items-center gap-2 ml-4">
<span className="text-xs font-medium text-brand-orange opacity-0 translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200">
{total > 0 ? `${Math.round((row.count / total) * 100)}%` : ''}
</span>
<span className="text-sm font-semibold text-neutral-600 dark:text-neutral-400 tabular-nums">
{formatNumber(row.count)}
</span>
</div>
</div>
))}
{Array.from({ length: emptySlots }).map((_, i) => (
<div key={`empty-${i}`} className="h-9 px-2 -mx-2" aria-hidden="true" />
))}
</div>
) : (