import React, { useEffect, useRef, useState } from 'react'; import { SCORE_WEIGHTS } from '../../lib/dashboard-score'; interface ScoreRingProps { score: number; grade: string; monthOverMonth?: number; size?: number; strokeWidth?: number; } interface RingTheme { stroke: string; number: string; grade: string; } const ringThemeForGrade = (grade: string): RingTheme => { switch (grade) { case 'A': return { stroke: '#10b981', number: 'text-emerald-600', grade: 'text-emerald-600', }; case 'B': return { stroke: '#84cc16', number: 'text-lime-600', grade: 'text-lime-600', }; case 'C': return { stroke: '#f59e0b', number: 'text-amber-600', grade: 'text-amber-600', }; case 'D': return { stroke: '#f97316', number: 'text-orange-600', grade: 'text-orange-600', }; default: return { stroke: '#ef4444', number: 'text-red-600', grade: 'text-red-600', }; } }; const THRESHOLD_BANDS: Array<{ range: string; label: string; pill: string }> = [ { range: '0-39', label: 'AI engines rarely cite you', pill: 'bg-red-50 text-red-600 border border-red-200', }, { range: '40-59', label: 'Inconsistent mentions', pill: 'bg-amber-50 text-amber-700 border border-amber-200', }, { range: '60-74', label: 'AI engines start citing you regularly', pill: 'bg-emerald-50 text-emerald-700 border border-emerald-200', }, { range: '75+', label: 'You hold position against competitors', pill: 'bg-[#ffeef8] text-[#B7007C] border border-[#B7007C]/20', }, ]; export function ScoreRing({ score, grade, monthOverMonth, size = 240, strokeWidth = 12, }: ScoreRingProps) { const radius: number = (size - strokeWidth) / 2; const circumference: number = 2 * Math.PI * radius; const clampedScore: number = Math.max(0, Math.min(100, score)); const offset: number = circumference - (clampedScore / 100) * circumference; const theme: RingTheme = ringThemeForGrade(grade); const [infoOpen, setInfoOpen] = useState(false); const popoverRef = useRef(null); useEffect(() => { if (!infoOpen) return; const handleClickOutside = (event: MouseEvent): void => { if ( popoverRef.current && event.target instanceof Node && !popoverRef.current.contains(event.target) ) { setInfoOpen(false); } }; const handleEscape = (event: KeyboardEvent): void => { if (event.key === 'Escape') setInfoOpen(false); }; document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; }, [infoOpen]); return (
{clampedScore}
{grade}
AI Readiness Score
{typeof monthOverMonth === 'number' && monthOverMonth !== 0 && (
0 ? 'bg-emerald-50 text-emerald-700' : 'bg-red-50 text-red-600' }`} > {monthOverMonth > 0 ? '↑' : '↓'} {Math.abs(monthOverMonth)} mo/mo
)}
{infoOpen && ( setInfoOpen(false)} /> )}
); } interface ScoreInfoPopoverProps { onClose: () => void; } function ScoreInfoPopover({ ref, onClose, }: ScoreInfoPopoverProps & { ref: React.RefObject }) { const composite: Array<{ label: string; weight: number }> = [ { label: 'AI Visibility', weight: SCORE_WEIGHTS.visibility }, { label: 'Catalog Quality', weight: SCORE_WEIGHTS.catalog }, { label: 'Agent Quality', weight: SCORE_WEIGHTS.agent }, ]; return (
} role="dialog" className="absolute left-1/2 top-full z-40 mt-3 w-[320px] -translate-x-1/2 rounded-2xl border border-gray-200 bg-white p-5 shadow-xl" >

The score AI engines use to decide whether to recommend your store.

Composite

    {composite.map(component => (
  • {component.label} {Math.round(component.weight * 100)}%
  • ))}

Thresholds

    {THRESHOLD_BANDS.map(band => (
  • {band.range} {band.label}
  • ))}
); }