diff --git a/CHANGELOG.md b/CHANGELOG.md index 3271cd8..4f887e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Improved -- **Redesigned Journeys page.** The depth slider now matches the rest of the UI and goes up to 10 steps. Controls are integrated into the chart card for a cleaner layout, and Top Paths are shown as visual breadcrumb cards instead of a cramped table. +- **Animated numbers across the dashboard.** Stats like visitors, pageviews, bounce rate, and visit duration now smoothly count up or down when you switch date ranges, apply filters, or when real-time visitor counts change — instead of just jumping to the new value. +- **Inline bar charts on dashboard lists.** Pages, referrers, locations, technology, and campaigns now show subtle proportional bars behind each row, making it easier to compare values at a glance without reading numbers. +- **Redesigned Top Paths.** The Top Paths section on the Journeys page has been completely rebuilt — from bulky cards to a clean, compact list with inline bars that matches the rest of Pulse. Long path sequences are truncated so they stay readable. +- **Redesigned Journeys page.** The depth slider now matches the rest of the UI and goes up to 10 steps. Controls are integrated into the chart card for a cleaner layout. - **More reliable visit duration tracking.** Visit duration was silently dropping to 0s for visitors who only viewed one page — especially on mobile or when closing a tab quickly. The tracking script now captures time-on-page more reliably across all browsers, and sessions where duration couldn't be measured are excluded from the average instead of counting as 0s. - **More accurate rage click detection.** Rage clicks no longer fire when you triple-click to select text on a page. Previously, selecting a paragraph (a normal 3-click action) was being counted as a rage click, which inflated frustration metrics. Only genuinely frustrated rapid clicking is tracked now. - **Fresher CDN data.** BunnyCDN statistics now refresh every 3 hours instead of once a day, so your CDN tab shows much more current bandwidth, request, and cache data. diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index 85ab438..7fc7bc8 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -10,6 +10,7 @@ import { Select, DownloadIcon, PlusIcon, XIcon } from '@ciphera-net/ui' import { Checkbox } from '@ciphera-net/ui' import { ArrowUpRight, ArrowDownRight } from '@phosphor-icons/react' import { motion } from 'framer-motion' +import { AnimatedNumber } from '@/components/ui/animated-number' import { cn } from '@/lib/utils' import { formatTime, formatDateShort, formatDate } from '@/lib/utils/formatDate' @@ -350,7 +351,7 @@ export default function Chart({ >
{m.label}
- {m.format(m.value)} + {m.change !== null && ( {m.isPositive ? : } diff --git a/components/dashboard/RealtimeVisitors.tsx b/components/dashboard/RealtimeVisitors.tsx index 852e636..b43356b 100644 --- a/components/dashboard/RealtimeVisitors.tsx +++ b/components/dashboard/RealtimeVisitors.tsx @@ -1,3 +1,7 @@ +'use client' + +import { AnimatedNumber } from '@/components/ui/animated-number' + interface RealtimeVisitorsProps { count: number } @@ -14,7 +18,7 @@ export default function RealtimeVisitors({ count }: RealtimeVisitorsProps) {
- {count} + v.toLocaleString()} />
) diff --git a/components/ui/animated-number.tsx b/components/ui/animated-number.tsx new file mode 100644 index 0000000..fad42df --- /dev/null +++ b/components/ui/animated-number.tsx @@ -0,0 +1,29 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { useMotionValue, useSpring, useTransform, motion } from 'framer-motion' + +interface AnimatedNumberProps { + value: number + format: (v: number) => string + className?: string +} + +export function AnimatedNumber({ value, format, className }: AnimatedNumberProps) { + const motionValue = useMotionValue(value) + const spring = useSpring(motionValue, { stiffness: 120, damping: 20, mass: 0.5 }) + const display = useTransform(spring, (v) => format(Math.round(v))) + const isFirst = useRef(true) + + useEffect(() => { + if (isFirst.current) { + // Skip animation on initial render — jump straight to value + motionValue.jump(value) + isFirst.current = false + } else { + motionValue.set(value) + } + }, [value, motionValue]) + + return {display} +}