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 | 5x 5x 24x 24x 24x 24x 24x 24x 5x 3x 24x 22x 24x | import { MouseEvent, useState } from "react";
import { Stack } from "@mui/material";
import { DotsThreeVerticalIcon } from "@phosphor-icons/react";
import { DonationType } from "@customTypes/products/fundraiser.types";
import ContextualMenu from "@shared/ContextualMenu/ContextualMenu";
import GiveIconButton from "@shared/IconButton/GiveIconButton";
import GiveExpandingSearch from "@shared/SearchBar/GiveExpandingSearch";
import GiveTruncateText from "@shared/Text/GiveTruncateText";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { styled } from "@theme/v2/Provider";
import { useEventListActions } from "./hooks/useEventListActions";
import { useEventMenuActions } from "./hooks/useEventMenuActions";
import { useMenuIcon } from "./hooks/useMenuIcon";
import { EventDetailTab } from "./constants";
const MENU_ARIA_LABEL = "Event options";
type Props = {
event?: DonationType;
eventId?: string;
tab: EventDetailTab;
search: string;
onSearchChange: (value: string) => void;
};
/**
* The Event Detail header once the page is scrolled — frames 8122-61108
* (Transactions), 8122-61150 (Tickets) and 8091-56253 (phone). Handed to
* `<Table CompactHead>`, which owns where it is drawn.
*/
const EventCompactBanner = ({
event,
eventId,
tab,
search,
onSearchChange,
}: Props) => {
const { isMobileView } = useCustomThemeV2();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const withIcon = useMenuIcon();
// `eventId` rather than the route's: on phones this banner is rendered by
// `MobileNavBar`, which is mounted outside the route tree.
const { menuOptions } = useEventMenuActions(event, eventId);
const { options: listOptions, filtersAmount } = useEventListActions({
eventId,
tab,
withIcon,
});
return (
<Container direction="row" alignItems="center" gap="16px">
{/* The phone frame moves the name into the top bar itself, leaving this
row to the full-width search field and the menu. */}
{!isMobileView && (
<Title
variant="bodyL"
color="primary"
lineClamp={1}
data-testid="event-compact-title"
>
{event?.name}
</Title>
)}
<Actions direction="row" alignItems="center">
<GiveExpandingSearch value={search} onChange={onSearchChange} />
<GiveIconButton
Icon={DotsThreeVerticalIcon}
// Filled while the list is narrowed, so the collapsed control still
// shows a filter is applied (as the phone toolbar's overflow does).
variant={filtersAmount > 0 ? "filled" : "ghost"}
aria-label={MENU_ARIA_LABEL}
data-testid="event-compact-menu-button"
onClick={(e: MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget)}
/>
</Actions>
{/* The mockup draws one (⋮) where the full header has a toolbar AND its
own (…), so the tab's list controls have to sit ahead of the event
actions or they become unreachable while scrolled. */}
<ContextualMenu
anchorEl={anchorEl}
handleClose={() => setAnchorEl(null)}
options={[
...listOptions,
{ type: "divider" as const, text: "" },
...menuOptions,
]}
color="primary"
texture={isMobileView ? "solid" : "blurred"}
menuWidth={276}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
/>
</Container>
);
};
export default EventCompactBanner;
const Container = styled(Stack)(() => ({
width: "100%",
minWidth: 0,
}));
const Title = styled(GiveTruncateText)(() => ({
flex: 1,
minWidth: 0,
}));
// Below the wide breakpoint `GiveExpandingSearch` renders a real field rather
// than a 40px icon, so the search has to be allowed to grow — capped while the
// name is still on the row, uncapped on phones where it owns the whole row.
// `:first-child`, not `:first-of-type`: the search is a <div> and the kebab a
// <button>, so per-type matching stretches the kebab too.
const Actions = styled(Stack)(({ theme }) => ({
gap: "8px",
flexShrink: 0,
[theme.breakpoints.down("new_lg")]: {
flexGrow: 1,
minWidth: 0,
justifyContent: "flex-end",
"& > *:first-child": {
flexGrow: 1,
minWidth: 0,
maxWidth: "320px",
},
},
[theme.breakpoints.down("v2_sm")]: {
"& > *:first-child": {
maxWidth: "none",
},
},
}));
|