All files / src/features/Events/EventDetail/reviews ReviewRatingDistribution.tsx

100% Statements 12/12
90% Branches 9/10
100% Functions 6/6
100% Lines 11/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121              19x                                                                     19x                 107x   535x   535x 535x       4x                                                                                       19x 3624x 515x                 515x          
import { Box, Stack } from "@mui/material";
import GiveCheckbox from "@shared/GiveCheckbox/GiveCheckbox";
import GiveText from "@shared/Text/GiveText";
import { styled } from "@theme/v2/Provider";
import { ReviewRatingFilter } from "./review.types";
 
/** Highest first, the way both frames draw it. */
const RATINGS: ReviewRatingFilter[] = [5, 4, 3, 2, 1];
 
type Props = {
  /** Reviews per star, straight off the summary read. */
  counts: Record<number, number>;
  /** The summary's own total — the denominator every bar is sized against. */
  total: number;
  selected: ReviewRatingFilter[];
  onToggle: (rating: ReviewRatingFilter) => void;
  /**
   * The bar's unfilled colour. The portal takes it from the theme; the public
   * event page passes a tint of its own adaptive colour, because a payment form
   * carries the merchant's background rather than a fixed palette.
   */
  trackColor?: string;
  /**
   * The summary read failed, so `counts` and `total` describe nothing. The rows
   * keep their checkboxes — the star filters are the reason this list is
   * interactive, and they work off the list read — but the bar and the
   * percentage are dropped rather than drawn at 0 %, which would read as a
   * genuine distribution.
   */
  countsUnavailable?: boolean;
  /** Set on the portal's rows; the public page's bars carry no test hooks. */
  testIdPrefix?: string;
};
 
/**
 * PayBuilder 034 — the 1-5 star breakdown, checkbox per row, the bar doubling
 * as the distribution (frames 7506-74961 / 7357-80180).
 *
 * One component for the Reviews tab's sidebar and the live event page: the
 * percentage rounding and the zero-total guard are the same fact in both
 * places, and had drifted apart once already.
 */
const ReviewRatingDistribution = ({
  counts,
  total,
  selected,
  onToggle,
  trackColor,
  countsUnavailable,
  testIdPrefix,
}: Props) => (
  <Stack gap="16px">
    {RATINGS.map((rating) => {
      const count = counts[rating] ?? 0;
      // Guard the zero-review event: 0/0 would size every bar NaN.
      const percent = total > 0 ? Math.round((count / total) * 100) : 0;
      return (
        <Stack key={rating} direction="row" gap="12px" alignItems="center">
          <GiveCheckbox
            checked={selected.includes(rating)}
            onChange={() => onToggle(rating)}
            inputProps={{ "aria-label": `${rating}-star` }}
            data-testid={
              testIdPrefix ? `${testIdPrefix}-rating-${rating}` : undefined
            }
          />
          <GiveText
            variant="bodyS"
            color="primary"
            sx={{ width: "47px", flexShrink: 0 }}
          >
            {rating}-star
          </GiveText>
          {!countsUnavailable && (
            <>
              <Track
                trackColor={trackColor}
                role="meter"
                aria-label={`${rating}-star reviews`}
                aria-valuenow={count}
                aria-valuemin={0}
                // An event with no reviews would otherwise declare min 0 /
                // max 0, which is not a valid ARIA range.
                aria-valuemax={Math.max(total, 1)}
              >
                <Fill sx={{ width: `${percent}%` }} />
              </Track>
              <GiveText
                variant="bodyS"
                color="secondary"
                sx={{ minWidth: "34px", textAlign: "right", flexShrink: 0 }}
              >
                {percent} %
              </GiveText>
            </>
          )}
        </Stack>
      );
    })}
  </Stack>
);
 
export default ReviewRatingDistribution;
 
const Track = styled(Box, {
  shouldForwardProp: (prop) => prop !== "trackColor",
})<{ trackColor?: string }>(({ theme, trackColor }) => ({
  position: "relative",
  flex: 1,
  height: "10px",
  borderRadius: "5px",
  backgroundColor: trackColor || theme.palette.primitive?.neutral?.["10"],
  overflow: "hidden",
}));
 
const Fill = styled(Box)(({ theme }) => ({
  height: "100%",
  borderRadius: "5px",
  backgroundColor: theme.palette.primitive?.blue?.["70"],
}));