Release 0.14.0-alpha #42

Merged
uz1mani merged 109 commits from staging into main 2026-03-12 12:12:03 +00:00
53 changed files with 5871 additions and 1199 deletions
Showing only changes of commit 88f02a244b - Show all commits

View File

@@ -26,12 +26,12 @@ import dynamic from 'next/dynamic'
import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons' import { DashboardSkeleton, useMinimumLoading } from '@/components/skeletons'
import FilterBar from '@/components/dashboard/FilterBar' import FilterBar from '@/components/dashboard/FilterBar'
import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown' import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown'
import Chart from '@/components/dashboard/Chart'
import ContentStats from '@/components/dashboard/ContentStats'
import TopReferrers from '@/components/dashboard/TopReferrers'
import Locations from '@/components/dashboard/Locations'
import TechSpecs from '@/components/dashboard/TechSpecs'
const Chart = dynamic(() => import('@/components/dashboard/Chart'))
const ContentStats = dynamic(() => import('@/components/dashboard/ContentStats'))
const TopReferrers = dynamic(() => import('@/components/dashboard/TopReferrers'))
const Locations = dynamic(() => import('@/components/dashboard/Locations'))
const TechSpecs = dynamic(() => import('@/components/dashboard/TechSpecs'))
const PerformanceStats = dynamic(() => import('@/components/dashboard/PerformanceStats')) const PerformanceStats = dynamic(() => import('@/components/dashboard/PerformanceStats'))
const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats')) const GoalStats = dynamic(() => import('@/components/dashboard/GoalStats'))
const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth')) const ScrollDepth = dynamic(() => import('@/components/dashboard/ScrollDepth'))

View File

@@ -5,6 +5,55 @@ import { createMap } from 'svg-dotted-map'
import { cn, formatNumber } from '@ciphera-net/ui' import { cn, formatNumber } from '@ciphera-net/ui'
import { countryCentroids } from '@/lib/country-centroids' import { countryCentroids } from '@/lib/country-centroids'
// ─── Module-level constants ────────────────────────────────────────
// Computed once when the module loads, survives component unmount/remount.
const MAP_WIDTH = 150
const MAP_HEIGHT = 68
const DOT_RADIUS = 0.25
const { points: MAP_POINTS, addMarkers } = createMap({ width: MAP_WIDTH, height: MAP_HEIGHT, mapSamples: 8000 })
// Pre-compute stagger helpers (row offsets for hex-grid pattern)
const _stagger = (() => {
const sorted = [...MAP_POINTS].sort((a, b) => a.y - b.y || a.x - b.x)
const rowMap = new Map<number, number>()
let step = 0
let prevY = Number.NaN
let prevXInRow = Number.NaN
for (const p of sorted) {
if (p.y !== prevY) {
prevY = p.y
prevXInRow = Number.NaN
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
}
if (!Number.isNaN(prevXInRow)) {
const delta = p.x - prevXInRow
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
}
prevXInRow = p.x
}
return { xStep: step || 1, yToRowIndex: rowMap }
})()
// Pre-compute the base map dots as a single SVG path string (~8000 circles → 1 path)
const BASE_DOTS_PATH = (() => {
const r = DOT_RADIUS
const d = r * 2
const parts: string[] = []
for (const point of MAP_POINTS) {
const rowIndex = _stagger.yToRowIndex.get(point.y) ?? 0
const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
const cx = point.x + offsetX
const cy = point.y
parts.push(`M${cx - r},${cy}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 ${-d},0`)
}
return parts.join('')
})()
// ─── Component ─────────────────────────────────────────────────────
interface DottedMapProps { interface DottedMapProps {
data: Array<{ country: string; pageviews: number }> data: Array<{ country: string; pageviews: number }>
className?: string className?: string
@@ -20,16 +69,8 @@ function getCountryName(code: string): string {
} }
export default function DottedMap({ data, className }: DottedMapProps) { export default function DottedMap({ data, className }: DottedMapProps) {
const width = 150
const height = 68
const dotRadius = 0.25
const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null) const [tooltip, setTooltip] = useState<{ x: number; y: number; country: string; pageviews: number } | null>(null)
const { points, addMarkers } = useMemo(
() => createMap({ width, height, mapSamples: 8000 }),
[width, height],
)
const markerData = useMemo(() => { const markerData = useMemo(() => {
if (!data.length) return [] if (!data.length) return []
@@ -47,55 +88,15 @@ export default function DottedMap({ data, className }: DottedMapProps) {
})) }))
}, [data]) }, [data])
const markerInputs = useMemo( const processedMarkers = useMemo(
() => markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size })), () => addMarkers(markerData.map((d) => ({ lat: d.lat, lng: d.lng, size: d.size }))),
[markerData], [markerData],
) )
const processedMarkers = useMemo(() => addMarkers(markerInputs), [addMarkers, markerInputs])
// Compute stagger helpers
const { xStep, yToRowIndex } = useMemo(() => {
const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x)
const rowMap = new Map<number, number>()
let step = 0
let prevY = Number.NaN
let prevXInRow = Number.NaN
for (const p of sorted) {
if (p.y !== prevY) {
prevY = p.y
prevXInRow = Number.NaN
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
}
if (!Number.isNaN(prevXInRow)) {
const delta = p.x - prevXInRow
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
}
prevXInRow = p.x
}
return { xStep: step || 1, yToRowIndex: rowMap }
}, [points])
// Batch all 8000 base dots into a single <path> instead of 8000 <circle> elements
const dotsPath = useMemo(() => {
const r = dotRadius
const d = r * 2
const parts: string[] = []
for (const point of points) {
const rowIndex = yToRowIndex.get(point.y) ?? 0
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0
const cx = point.x + offsetX
const cy = point.y
parts.push(`M${cx - r},${cy}a${r},${r} 0 1,0 ${d},0a${r},${r} 0 1,0 ${-d},0`)
}
return parts.join('')
}, [points, dotRadius, xStep, yToRowIndex])
return ( return (
<div className="relative w-full h-full flex items-center justify-center"> <div className="relative w-full h-full flex items-center justify-center">
<svg <svg
viewBox={`0 0 ${width} ${height}`} viewBox={`0 0 ${MAP_WIDTH} ${MAP_HEIGHT}`}
className={cn('text-neutral-400 dark:text-neutral-500', className)} className={cn('text-neutral-400 dark:text-neutral-500', className)}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
> >
@@ -110,18 +111,18 @@ export default function DottedMap({ data, className }: DottedMapProps) {
</filter> </filter>
</defs> </defs>
<path <path
d={dotsPath} d={BASE_DOTS_PATH}
fill="currentColor" fill="currentColor"
/> />
{processedMarkers.map((marker, index) => { {processedMarkers.map((marker, index) => {
const rowIndex = yToRowIndex.get(marker.y) ?? 0 const rowIndex = _stagger.yToRowIndex.get(marker.y) ?? 0
const offsetX = rowIndex % 2 === 1 ? xStep / 2 : 0 const offsetX = rowIndex % 2 === 1 ? _stagger.xStep / 2 : 0
const info = markerData[index] const info = markerData[index]
return ( return (
<circle <circle
cx={marker.x + offsetX} cx={marker.x + offsetX}
cy={marker.y} cy={marker.y}
r={marker.size ?? dotRadius} r={marker.size ?? DOT_RADIUS}
fill="#FD5E0F" fill="#FD5E0F"
filter="url(#marker-glow)" filter="url(#marker-glow)"
className="cursor-pointer" className="cursor-pointer"
@@ -132,8 +133,8 @@ export default function DottedMap({ data, className }: DottedMapProps) {
const svgX = marker.x + offsetX const svgX = marker.x + offsetX
const svgY = marker.y const svgY = marker.y
setTooltip({ setTooltip({
x: rect.left + (svgX / width) * rect.width, x: rect.left + (svgX / MAP_WIDTH) * rect.width,
y: rect.top + (svgY / height) * rect.height, y: rect.top + (svgY / MAP_HEIGHT) * rect.height,
country: info.country, country: info.country,
pageviews: info.pageviews, pageviews: info.pageviews,
}) })