All files / src/features/Events/EventDetail/inventory EventInventoryCard.tsx

100% Statements 21/21
100% Branches 14/14
100% Functions 6/6
100% Lines 15/15

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              5x 5x 5x                         5x   10x 10x 10x 10x 8x 7x     10x                                                                                                       10x                                         10x                                 9x                 9x                
import { Box, Stack } from "@mui/material";
import { TicketIcon } from "@phosphor-icons/react";
import GiveText from "@shared/Text/GiveText";
import GiveTruncateText from "@shared/Text/GiveTruncateText";
import { styled } from "@theme/v2/Provider";
import { EventInventoryRow } from "./inventory.types";
 
const TICKETS_SOLD_SUFFIX = "tickets sold";
const UNLIMITED_AVAILABLE = "Unlimited tickets available";
const TICKETS_AVAILABLE_SUFFIX = "tickets available";
 
type Props = {
  row: EventInventoryRow;
};
 
/**
 * One ticket type on the Inventory tab (frames 7278-37177 / 7278-37230).
 *
 * Count colour: neutral at zero sold (frame 7290-37794), green when sold out,
 * blue otherwise. Zero wins over sold-out so a 0-capacity card doesn't print a
 * green 0.
 */
const EventInventoryCard = ({ row }: Props) => {
  const { name, priceLabel, sold, total, available, isSoldOut, isUnlimited } =
    row;
  const percentSold = total > 0 ? Math.min(100, (sold / total) * 100) : 0;
  const countClass = (() => {
    if (sold === 0) return "zero";
    if (isSoldOut) return "sold-out";
    return undefined;
  })();
 
  return (
    <Card data-testid="event-inventory-card">
      <Stack direction="row" alignItems="center" gap="16px" overflow="hidden">
        <GiveTruncateText lineClamp={1} variant="bodyM" sx={{ flex: 1 }}>
          {name}
        </GiveTruncateText>
        <GiveText variant="bodyS" color="secondary" whiteSpace="nowrap">
          {priceLabel}
        </GiveText>
      </Stack>
 
      <Stack gap="8px" width="100%">
        <Stack direction="row" alignItems="baseline" gap="8px">
          <SoldCount
            data-testid="event-inventory-sold-count"
            className={countClass}
          >
            {sold}
          </SoldCount>
          <GiveText variant="bodyS" color="secondary" whiteSpace="nowrap">
            {isUnlimited
              ? TICKETS_SOLD_SUFFIX
              : `of ${total} ${TICKETS_SOLD_SUFFIX}`}
          </GiveText>
        </Stack>
        {/* An uncapped ticket type has no ratio to draw. */}
        {!isUnlimited && (
          <ProgressTrack>
            <ProgressFill
              className={isSoldOut ? "sold-out" : undefined}
              style={{ width: `${percentSold}%` }}
            />
          </ProgressTrack>
        )}
      </Stack>
 
      <Stack direction="row" alignItems="center" gap="8px">
        <TicketIcon size={16} />
        {/* Frame 8114-58783's annotation is explicit: the word "Unlimited",
            not the infinity glyph this used to draw. */}
        <GiveText variant="bodyS" whiteSpace="nowrap">
          {isUnlimited
            ? UNLIMITED_AVAILABLE
            : `${available} ${TICKETS_AVAILABLE_SUFFIX}`}
        </GiveText>
      </Stack>
    </Card>
  );
};
 
export default EventInventoryCard;
 
const Card = styled(Box)(({ theme }) => ({
  display: "flex",
  flexDirection: "column",
  gap: "16px",
  padding: "20px",
  borderRadius: "16px",
  overflow: "hidden",
  backgroundColor: theme.palette.surface?.primary,
  // The mockup's card outline is a flat 10% black rather than the border token
  // the rest of the page uses, so it stays legible over the banner backdrop.
  border: `1px solid ${theme.palette.primitive?.transparent?.["darken-10"]}`,
}));
 
// Desktop/H3 — 28/34 Light, -3% tracking. Spelled out rather than the `h3`
// variant so the card is immune to a heading-scale change elsewhere, but built
// on GiveText rather than a raw element so the number is a design-system text.
//
// `&&` doubles the specificity: GiveText passes its `color` down as a
// Typography system prop, which lands in `sx` and would otherwise outrank a
// plain rule here. The three states are its own (blue-70 is not one of the
// colour names GiveText resolves).
const SoldCount = styled(GiveText)(({ theme }) => ({
  "&&": {
    margin: 0,
    fontSize: "28px",
    lineHeight: "34px",
    fontWeight: 300,
    letterSpacing: "-0.84px",
    color: theme.palette.primitive?.blue?.[70],
  },
  "&&.sold-out": {
    color: theme.palette.primitive?.success?.[50],
  },
  "&&.zero": {
    color: theme.palette.text?.primary,
  },
}));
 
const ProgressTrack = styled(Box)(({ theme }) => ({
  position: "relative",
  height: "3px",
  width: "100%",
  borderRadius: "8px",
  overflow: "hidden",
  backgroundColor: theme.palette.surface?.tertiary,
}));
 
const ProgressFill = styled(Box)(({ theme }) => ({
  height: "100%",
  borderRadius: "2px",
  backgroundColor: theme.palette.primitive?.blue?.[50],
  "&.sold-out": {
    backgroundColor: theme.palette.primitive?.success?.[50],
  },
}));