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"],
}));
|