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 | 9x 9x 83x 83x 166x 151x 151x 83x 83x 83x 83x 8x 7x 83x 83x 83x 77x 9x 101x 77x 101x 101x | import { Stack } from "@mui/material";
import { CalendarBlankIcon, Icon, MapPinIcon } from "@phosphor-icons/react";
import { DonationType } from "@customTypes/products/fundraiser.types";
import GiveText from "@shared/Text/GiveText";
import { styled } from "@theme/v2/Provider";
import { useFormatDateInTimezone } from "@utils/date.helpers";
import { ReactNode } from "react";
import { VIEWPORT_WIDTH_CAP } from "./constants";
const EVENT_DATE_FORMAT = "MMMM d, yyyy hh:mm a";
type Props = {
event?: DonationType;
};
/**
* PayBuilder 032-b — the Date and Location cards that sit between the Event
* Detail header and the tabs.
*
* Timestamps are epoch *seconds* and are formatted through
* `useFormatDateInTimezone` (platform timezone, never a hardcoded offset). A
* null/0 timestamp means "unset" and is skipped rather than rendered as 1970.
*/
const EventInfoCards = ({ event }: Props) => {
const { formatInTimezone } = useFormatDateInTimezone();
// `formatInTimezone` is typed loosely (it accepts Dates and strings too), so
// narrow the result to the string this card renders.
const format = (timestamp?: number | null) => {
if (!timestamp) return null;
const formatted = formatInTimezone(timestamp, EVENT_DATE_FORMAT);
return formatted ? String(formatted) : null;
};
const startsAt = format(event?.startsAt);
const endsAt = format(event?.endsAt);
const getDateLabel = () => {
if (startsAt && endsAt) return `From ${startsAt} to ${endsAt}`;
if (startsAt) return startsAt;
return null;
};
const dateLabel = getDateLabel();
const location = event?.locationShortAddress;
if (!dateLabel && !location) return null;
return (
<CardsRow direction="row" gap="16px">
{dateLabel && (
<InfoCard title="Date" Glyph={CalendarBlankIcon} testId="event-date">
{dateLabel}
</InfoCard>
)}
{location && (
<InfoCard title="Location" Glyph={MapPinIcon} testId="event-location">
{location}
</InfoCard>
)}
</CardsRow>
);
};
const InfoCard = ({
title,
Glyph,
children,
testId,
}: {
title: string;
Glyph: Icon;
children: ReactNode;
testId: string;
}) => (
<Card gap="20px" data-testid={testId}>
<GiveText variant="h4" color="primary">
{title}
</GiveText>
<Stack direction="row" gap="12px" alignItems="center" width="100%">
<IconBubble>
<Glyph size={22} />
</IconBubble>
<GiveText variant="bodyS" color="primary" flex="1 0 0" minWidth={0}>
{children}
</GiveText>
</Stack>
</Card>
);
export default EventInfoCards;
// Desktop wraps the cards; mobile keeps them on one line and scrolls
// horizontally, so the second card peeks in from the right edge as it does in
// the mockup.
//
// `minWidth: 0` is load-bearing: a flex item defaults to `min-width: auto`, so
// without it this row would size to its content and widen the whole page
// instead of scrolling inside it.
const CardsRow = styled(Stack)(({ theme }) => ({
flexWrap: "wrap",
width: "100%",
minWidth: 0,
maxWidth: "100%",
[theme.breakpoints.down("v2_sm")]: {
flexWrap: "nowrap",
overflowX: "auto",
overscrollBehaviorX: "contain",
// On mobile this row lives inside the transactions table's <caption>, and
// that table is `table-layout: auto` — its width comes from its content's
// max-content size. Two 288px cards therefore widened the table past the
// viewport and the whole table (banner, tabs and rows together) scrolled
// sideways. The viewport cap is a definite max-width, so the row can never
// report more than the screen and scrolls internally instead.
maxWidth: VIEWPORT_WIDTH_CAP,
scrollbarWidth: "none",
"&::-webkit-scrollbar": { display: "none" },
},
}));
// Per the mockup the cards are an outline only — no fill — so the animated
// banner backdrop stays visible behind them.
const Card = styled(Stack)(({ theme }) => ({
flex: "0 0 324px",
minWidth: 0,
maxWidth: "324px",
padding: "20px",
borderRadius: "16px",
border: `1px solid ${theme.palette.primitive?.transparent["darken-10"]}`,
[theme.breakpoints.down("v2_sm")]: {
flex: "0 0 292px",
maxWidth: "292px",
},
}));
const IconBubble = styled(Stack)(({ theme }) => ({
width: "36px",
height: "36px",
flexShrink: 0,
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
backgroundColor: theme.palette.primitive?.transparent["darken-5"],
}));
|