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

100% Statements 10/10
100% Branches 2/2
100% Functions 4/4
100% Lines 7/7

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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                                                  6x                               6x               98x               98x                                                                                                                                                         95x                     95x         196x            
import { Box, Stack } from "@mui/material";
import { WarningIcon } from "@phosphor-icons/react";
import GiveAlert from "@shared/GiveAlert/GiveAlert";
import GiveStarRating from "@shared/StarRating/GiveStarRating";
import GiveSwitch from "@shared/Switch/GiveSwitch";
import GiveText from "@shared/Text/GiveText";
import { styled } from "@theme/v2/Provider";
import { formatAverage, reviewCountLabel } from "./EventReviewSummary";
import ReviewRatingDistribution from "./ReviewRatingDistribution";
import { EventReviewStats, ReviewRatingFilter } from "./review.types";
 
type Props = {
  stats: EventReviewStats;
  /**
   * The summary read failed. It is a separate request from the list, so this
   * column can be the only thing on the tab with nothing to show.
   */
  isStatsError?: boolean;
  ratings: ReviewRatingFilter[];
  awaitingReplyOnly: boolean;
  onToggleRating: (rating: ReviewRatingFilter) => void;
  onToggleAwaitingReply: () => void;
};
 
export const STATS_UNAVAILABLE =
  "The ratings summary couldn't be loaded. The reviews below are unaffected.";
 
/**
 * The Reviews tab's right-hand column (frames 7501-72341 / 7508-77120):
 * the event's overall score, the awaiting-reply toggle, and a checkbox per star
 * whose bar doubles as the distribution.
 *
 * The distribution is read from the summary endpoint, so it keeps describing
 * the whole event while the checkboxes narrow the list beside it.
 *
 * That endpoint failing is not the same as the event scoring zero. The score,
 * the stars and the bars all come out of it, and the hook's fallback stats are
 * zeros — so on a failure they are not drawn at all: a `0.00` and five `0 %`
 * bars sitting beside a full list would be the tab inventing the event's rating.
 * The filters stay, because they act on the list read, which is fine.
 */
const EventReviewSidebar = ({
  stats,
  isStatsError,
  ratings,
  awaitingReplyOnly,
  onToggleRating,
  onToggleAwaitingReply,
}: Props) => {
  const counts: Record<number, number> = {
    1: stats.numRating1,
    2: stats.numRating2,
    3: stats.numRating3,
    4: stats.numRating4,
    5: stats.numRating5,
  };
 
  return (
    <Stack gap="32px" data-testid="event-review-sidebar">
      {isStatsError ? (
        <GiveAlert
          type="warning"
          variant="notice"
          Icon={<WarningIcon size={20} />}
          description={STATS_UNAVAILABLE}
          dataTestId="event-review-stats-error"
        />
      ) : (
        <Summary>
          <Stack direction="row" gap="8px" alignItems="baseline">
            <GiveText variant="h5" color="primary">
              {formatAverage(stats.averageRating)}
            </GiveText>
            <GiveText variant="bodyS" color="secondary">
              {reviewCountLabel(stats.total)}
            </GiveText>
          </Stack>
          <Stars>
            <GiveStarRating value={stats.averageRating} size={24} />
          </Stars>
        </Summary>
      )}
 
      <Stack gap="16px">
        {/* "Turn on the toggle to show only comments that haven't been replied
            to yet" — annotation on frame 7506-74967. */}
        <FilterCard>
          <Stack
            direction="row"
            justifyContent="space-between"
            alignItems="center"
            gap="12px"
          >
            <GiveText variant="bodyS" color="primary">
              Awaiting reply from me
            </GiveText>
            <GiveSwitch
              checked={awaitingReplyOnly}
              onChange={onToggleAwaitingReply}
              inputProps={{ "aria-label": "Awaiting reply from me" }}
              data-testid="event-review-awaiting-toggle"
            />
          </Stack>
        </FilterCard>
 
        {/* "Clicking checkbox will filter the comments by the star" —
            annotation on frame 7506-74961. The rows themselves are shared with
            the live event page — see ReviewRatingDistribution. */}
        <FilterCard>
          <ReviewRatingDistribution
            counts={counts}
            total={stats.total}
            selected={ratings}
            onToggle={onToggleRating}
            countsUnavailable={isStatsError}
            testIdPrefix="event-review-filter"
          />
        </FilterCard>
      </Stack>
    </Stack>
  );
};
 
export default EventReviewSidebar;
 
/**
 * Stacked while the sidebar is a narrow column (frame 7501-73473: the score on
 * one line, the stars below it). Once it spans the page the mockup puts all
 * three on one row with the stars leading (frame 7506-74235) — which is also
 * where the vertical stack starts to look like wasted height.
 *
 * The public event page keeps the stacked form at every width (frame
 * 7508-78240), so this belongs here rather than in a shared summary.
 */
const Summary = styled(Box)(({ theme }) => ({
  display: "flex",
  flexDirection: "column",
  gap: "12px",
  [theme.breakpoints.down("v2_md")]: {
    flexDirection: "row",
    alignItems: "center",
    flexWrap: "wrap",
  },
}));
 
const Stars = styled(Box)(({ theme }) => ({
  lineHeight: 0,
  [theme.breakpoints.down("v2_md")]: { order: -1 },
}));
 
const FilterCard = styled(Box)(({ theme }) => ({
  padding: "16px",
  borderRadius: `${theme?.customs?.radius?.medium}px`,
  border: `1px solid ${theme.palette.border?.primary}`,
  backgroundColor: theme.palette.background.paper,
}));