'use client' import { useState, useEffect, useMemo, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { logger } from '@/lib/utils/logger' import { getDailyStats } from '@/lib/api/stats' import type { DailyStat } from '@/lib/api/stats' interface PeakHoursProps { siteId: string dateRange: { start: string, end: string } } const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] const HOUR_LABELS: Record = { 0: '12am', 6: '6am', 12: '12pm', 18: '6pm' } function formatHour(hour: number): string { if (hour === 0) return '12am' if (hour === 12) return '12pm' return hour < 12 ? `${hour}am` : `${hour - 12}pm` } export default function PeakHours({ siteId, dateRange }: PeakHoursProps) { const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(true) const [animKey, setAnimKey] = useState(0) const [hovered, setHovered] = useState<{ day: number; hour: number } | null>(null) const [tooltipPos, setTooltipPos] = useState<{ x: number; y: number } | null>(null) const gridRef = useRef(null) useEffect(() => { const fetchData = async () => { setIsLoading(true) try { const result = await getDailyStats(siteId, dateRange.start, dateRange.end, 'hour') setData(result) setAnimKey(k => k + 1) } catch (e) { logger.error(e) } finally { setIsLoading(false) } } fetchData() }, [siteId, dateRange]) const { grid, max, dayTotals, hourTotals, weekTotal } = useMemo(() => { const grid: number[][] = Array.from({ length: 7 }, () => Array(24).fill(0)) for (const d of data) { const date = new Date(d.date) const day = date.getDay() const hour = date.getHours() const adjustedDay = day === 0 ? 6 : day - 1 grid[adjustedDay][hour] += d.pageviews } const max = Math.max(...grid.flat(), 1) const dayTotals = grid.map(hours => hours.reduce((a, b) => a + b, 0)) const hourTotals = Array.from({ length: 24 }, (_, h) => grid.reduce((a, row) => a + row[h], 0)) const weekTotal = dayTotals.reduce((a, b) => a + b, 0) return { grid, max, dayTotals, hourTotals, weekTotal } }, [data]) const hasData = data.some(d => d.pageviews > 0) const bestTime = useMemo(() => { if (!hasData) return null let bestDay = 0, bestHour = 0, bestVal = 0 for (let d = 0; d < 7; d++) { for (let h = 0; h < 24; h++) { if (grid[d][h] > bestVal) { bestVal = grid[d][h] bestDay = d bestHour = h } } } return { day: bestDay, hour: bestHour } }, [grid, hasData]) const tooltipData = useMemo(() => { if (!hovered) return null const { day, hour } = hovered const value = grid[day][hour] const dayTotal = dayTotals[day] const hourTotal = hourTotals[hour] const pct = weekTotal > 0 ? Math.round((value / weekTotal) * 100) : 0 return { value, dayTotal, hourTotal, pct } }, [hovered, grid, dayTotals, hourTotals, weekTotal]) const handleCellMouseEnter = ( e: React.MouseEvent, dayIdx: number, hour: number ) => { setHovered({ day: dayIdx, hour }) if (gridRef.current) { const gridRect = gridRef.current.getBoundingClientRect() const cellRect = (e.currentTarget as HTMLDivElement).getBoundingClientRect() setTooltipPos({ x: cellRect.left - gridRect.left + cellRect.width / 2, y: cellRect.top - gridRect.top, }) } } return (

Peak Hours

When your visitors are most active

{isLoading ? (
{Array.from({ length: 7 }).map((_, i) => (
))}
) : hasData ? ( <>
{grid.map((hours, dayIdx) => (
{DAYS[dayIdx]}
{hours.map((value, hour) => { const isHoveredCell = hovered?.day === dayIdx && hovered?.hour === hour return ( handleCellMouseEnter(e, dayIdx, hour)} onMouseLeave={() => { setHovered(null); setTooltipPos(null) }} /> ) })}
))} {/* Hour axis labels */}
{Object.entries(HOUR_LABELS).map(([h, label]) => ( {label} ))} 12am
{/* Cell-anchored tooltip */} {hovered && tooltipData && tooltipPos && (
{DAYS[hovered.day]} {formatHour(hovered.hour)}
{tooltipData.value.toLocaleString()} pageviews {tooltipData.pct}% of week's traffic
{/* Arrow */}
)}
{/* Best time callout */} {bestTime && ( Your busiest time is{' '} {DAYS[bestTime.day]}s at {formatHour(bestTime.hour)} )} ) : (

No data available for this period

)}
) }