/** * The visibility score's bands, in one place. The score ring on the dashboard * and the week's canvas on HQ both colour a score, and a threshold copied * into two files is a threshold that will disagree with itself. */ /** One band of the 0-100 visibility score. */ export interface ScoreBand { /** Inclusive lower bound. */ min: number; /** Range as shown in a legend. */ range: string; /** What a score in this band means for the merchant. */ label: string; /** Text colour for a number in this band. */ text: string; /** Pill classes for a legend row. */ pill: string; } /** The bands, highest first, so the first match wins. */ export const SCORE_BANDS: readonly ScoreBand[] = [ { min: 75, range: '75+', label: 'You hold position against competitors', text: 'text-brand', pill: 'border border-pink-500/30 bg-pink-500/15 text-brand', }, { min: 60, range: '60-74', label: 'AI engines start citing you regularly', text: 'text-emerald-600', pill: 'border border-emerald-500/30 bg-emerald-500/15 text-emerald-600', }, { min: 40, range: '40-59', label: 'Inconsistent mentions', text: 'text-amber-600', pill: 'border border-amber-500/30 bg-amber-500/15 text-amber-600', }, { min: 0, range: '0-39', label: 'AI engines rarely cite you', text: 'text-red-600', pill: 'border border-red-500/30 bg-red-500/15 text-red-600', }, ]; /** The bands low to high, for a legend that reads upwards. */ export const SCORE_BANDS_ASCENDING: readonly ScoreBand[] = [ ...SCORE_BANDS, ].reverse(); /** * The band a score falls in. * * @param score - The visibility score, 0-100. * @return Its band; the lowest one for anything under the first bound. */ export function bandForScore(score: number): ScoreBand { const clamped: number = Math.max(0, Math.min(100, Math.round(score))); return ( SCORE_BANDS.find(band => clamped >= band.min) ?? SCORE_BANDS[SCORE_BANDS.length - 1] ); }