import React, { useState } from 'react'; import EmptyState from '../agent-analytics/EmptyState'; import type { CompetitorSnapshot } from '../../service/visibility/visibility.interface'; import { faviconUrlFor, formatDelta, toneToBadgeClass } from './helpers'; import ConfirmExcludeCompetitorModal from './ConfirmExcludeCompetitorModal'; /** Rows shown before "Load more"; the long tail is revealed on demand. * Manually tracked rivals are always kept regardless of rank. */ const INITIAL_LEADERBOARD_ROWS = 6; /** How many extra rows each "Load more" click reveals. */ const LEADERBOARD_ROWS_STEP = 6; /** Props for {@link CompetitorsTable}. */ interface CompetitorsTableProps { /** Competitor snapshots for the selected week (ordered by SOV desc). */ competitors: CompetitorSnapshot[]; /** Merchant's own brand snapshot, pinned at the top of the * leaderboard. Shown even when share-of-voice is 0% so the merchant * always sees their own row alongside the rivals. */ ownBrand?: { name: string; domain: string; shareOfVoice: number; deltaSov: number | null; mentionsCount: number | null; } | null; /** Recomaze client id, passed to the per-row delete action. */ clientId: string; /** Recomaze JWT, passed to the per-row delete action. */ token: string; /** Brand the leaderboard belongs to. */ brandId: string; /** * Fired after a competitor is excluded so the parent can refetch the * leaderboard. The backend already filters excluded domains, so a plain * refetch removes the row from the table. */ onCompetitorExcluded: () => void; } /** Props for {@link SampleQueriesPopover}. */ interface SampleQueriesPopoverProps { /** Localized sample queries for one competitor; render verbatim. */ queries: string[]; } /** * Click-to-open list of the sample queries that surfaced one competitor. * * @param {SampleQueriesPopoverProps} props - Popover props. * @returns {JSX.Element} Trigger + absolute-positioned panel. */ const SampleQueriesPopover = ({ queries, }: SampleQueriesPopoverProps): JSX.Element => { const [active, setActive] = useState(false); if (queries.length === 0) { return -; } return (
{active && ( <>
setActive(false)} aria-hidden="true" />
    {queries.map((query, idx) => (
  • {query}
  • ))}
)}
); }; /** * Competitor leaderboard: Domain | SOV | Δ vs last week | Mentions | * Sample queries | Actions. Rows the merchant manually tracks carry a * badge. The last column holds a delete button that suppresses the * domain from the leaderboard and from future scans. * * @param {CompetitorsTableProps} props - Table props. * @returns {JSX.Element} The leaderboard table. */ const CompetitorsTable = ({ competitors, ownBrand, clientId, token, brandId, onCompetitorExcluded, }: CompetitorsTableProps): JSX.Element => { const [excludeTarget, setExcludeTarget] = useState( null ); const [visibleCount, setVisibleCount] = useState(INITIAL_LEADERBOARD_ROWS); if (competitors.length === 0 && !ownBrand) { return ; } const topRows = competitors.slice(0, visibleCount); const seen = new Set(topRows.map(row => row.competitor_domain)); const visibleCompetitors = [ ...topRows, ...competitors.filter( row => row.manually_tracked && !seen.has(row.competitor_domain) ), ]; const shownDomains = new Set( visibleCompetitors.map(row => row.competitor_domain) ); const remainingCount = competitors.filter( row => !shownDomains.has(row.competitor_domain) ).length; return ( <>
{ownBrand && (() => { const ownDelta = formatDelta(ownBrand.deltaSov, '%'); return ( ); })()} {visibleCompetitors.map(competitor => { const delta = formatDelta(competitor.delta_vs_prev, '%'); const displayName = competitor.competitor_name || competitor.competitor_domain; return ( ); })}
Domain SOV Δ vs last week Mentions Sample queries
{ ( event.currentTarget as HTMLImageElement ).style.visibility = 'hidden'; }} /> {ownBrand.name} Your brand
{ownBrand.shareOfVoice}% {ownDelta.hidden ? ( - ) : ( {ownDelta.label} )} {ownBrand.mentionsCount ?? '-'} See “Tracked prompts” below
{ ( event.currentTarget as HTMLImageElement ).style.visibility = 'hidden'; }} /> {displayName} {competitor.manually_tracked && ( Manually tracked )}
{competitor.share_of_voice}% {delta.hidden ? ( - ) : ( {delta.label} )} {competitor.mentions_count}
{remainingCount > 0 && (
)}
setExcludeTarget(null)} onExcluded={onCompetitorExcluded} clientId={clientId} token={token} brandId={brandId} /> ); }; export default CompetitorsTable;