All files / src/features/Events/EventDetail EventDetailHeader.tsx

100% Statements 13/13
100% Branches 16/16
100% Functions 5/5
100% Lines 10/10

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                                                  11x 180x 180x   180x   180x 16x       180x           180x                                                                                                                                                   180x       180x                    
import { Box, Stack } from "@mui/material";
import { DotsThreeIcon, PaletteIcon } from "@phosphor-icons/react";
import { DonationType } from "@customTypes/products/fundraiser.types";
import GiveIconButton from "@shared/IconButton/GiveIconButton";
import GiveLargeIconButton from "@shared/LargeIconButton/GiveLargeIconButton";
import GiveThumbnail from "@shared/Thumbnail/GiveThumbnail";
import GiveTruncateText from "@shared/Text/GiveTruncateText";
import ContextualMenu from "@shared/ContextualMenu/ContextualMenu";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { styled } from "@theme/v2/Provider";
import { MouseEvent, useState } from "react";
import { EDIT_DENY_MESSAGE } from "@constants/permissions";
import { useEventMenuActions } from "./hooks/useEventMenuActions";
 
type Props = {
  event?: DonationType;
};
 
/**
 * PayBuilder 032-b AC001 — Event Detail header: thumbnail, event name, the
 * (…) contextual menu next to the title, and the "Edit Event" action.
 *
 * The header is rendered above both tabs, so the menu is reachable from the
 * Transactions and the Tickets tab alike (AC001.1).
 */
const EventDetailHeader = ({ event }: Props) => {
  const { isMobileView } = useCustomThemeV2();
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
  const { menuOptions, editPaymentForm, isEditDisabled, isEditAllowed } =
    useEventMenuActions(event);
 
  const handleOpenMenu = (e: MouseEvent<HTMLElement>) =>
    setAnchorEl(e.currentTarget);
 
  // AC001.4 — Esc and click-away are handled by the underlying Popover;
  // selecting an option closes the menu through `handleClose`.
  const handleCloseMenu = () => setAnchorEl(null);
 
  // Mobile stacks the thumbnail above the title and drops the "Edit Event"
  // button — the action stays reachable through the (…) sheet. Per the 390px
  // frame the thumbnail stays 72px and the title stays h3; only the layout
  // changes (thumbnail y188, title 20px below at y280).
  return (
    <Container
      direction={isMobileView ? "column" : "row"}
      alignItems={isMobileView ? "flex-start" : "center"}
      gap={isMobileView ? "20px" : "12px"}
    >
      <GiveThumbnail
        type="event"
        size="large"
        // The API's imageURL is a base path — a size segment must be appended
        // or the request 404s and the thumbnail falls back to the type icon.
        // `/thumb` is 80x80, which the 72px "large" thumbnail upscales on any
        // retina screen; `/medium` (400x400) stays sharp up to a 3x DPR.
        imageUrl={event?.imageURL ? `${event.imageURL}/medium` : undefined}
        name={event?.name}
      />
 
      <TitleStack
        direction="row"
        alignItems={isMobileView ? "flex-start" : "center"}
        gap={isMobileView ? "20px" : "12px"}
      >
        <GiveTruncateText
          variant="h3"
          color="primary"
          lineClamp={2}
          data-testid="event-detail-title"
        >
          {event?.name}
        </GiveTruncateText>
        <GiveIconButton
          Icon={DotsThreeIcon}
          variant="ghost"
          size="large"
          aria-label="Event actions"
          data-testid="event-detail-menu-button"
          onClick={handleOpenMenu}
        />
      </TitleStack>
 
      {!isMobileView && (
        <Box data-testid="event-detail-edit-button">
          <GiveLargeIconButton
            Icon={PaletteIcon}
            label="Edit Event"
            onClick={editPaymentForm}
            disabled={isEditDisabled}
            // The deny message is about permission only — an ended (or still
            // loading) event also disables Edit, and hovering that must not
            // claim the merchant lacks access (same split as the menu item).
            tooltipProps={{
              title: EDIT_DENY_MESSAGE,
              disableHoverListener: isEditAllowed,
            }}
          />
        </Box>
      )}
 
      <ContextualMenu
        anchorEl={anchorEl}
        handleClose={handleCloseMenu}
        options={menuOptions}
        color="primary"
        texture={isMobileView ? "solid" : "blurred"}
        menuWidth={276}
        anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
        transformOrigin={{ vertical: "top", horizontal: "left" }}
      />
    </Container>
  );
};
 
export default EventDetailHeader;
 
const Container = styled(Stack)(() => ({
  width: "100%",
}));
 
const TitleStack = styled(Stack)(({ theme }) => ({
  flex: 1,
  minWidth: 0,
  [theme.breakpoints.down("v2_sm")]: {
    // Column layout: the row has to span the header so the (…) button stays
    // pinned to the right of the wrapping title.
    width: "100%",
    justifyContent: "space-between",
  },
}));