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 | 4x 4x 27x 27x 1x 27x | import { Box, Stack } from "@mui/material";
import { ChatCircleIcon } from "@phosphor-icons/react";
import GiveAvatar from "@shared/Avatar/GiveAvatar";
import GiveLink from "@shared/Link/GiveLink";
import GiveStarRating from "@shared/StarRating/GiveStarRating";
import GiveText from "@shared/Text/GiveText";
import { styled } from "@theme/v2/Provider";
import { formatReviewDate } from "./reviewDate";
import { EventReviewRow } from "./review.types";
type Props = {
review: EventReviewRow;
/**
* Whether this user may publish replies — the product-update grant. No
* default: a permission gate that falls open when a caller forgets it is
* worse than a type error.
*/
canReply: boolean;
/** Follows the link through to the Reviews tab. */
onReply?: (review: EventReviewRow) => void;
onViewReply?: (review: EventReviewRow) => void;
};
const COMMENT_CLAMP_LINES = 3;
/**
* One customer review as the side panel previews it (frames 7878-57940 /
* 7878-56665). The Reviews tab has its own row — EventReviewListItem — because
* it carries the composer and the published reply, which do not fit a
* fixed-height preview card.
*
* A review with no comment still renders — stars, name and date, with no empty
* comment block and no placeholder text standing in for words the customer
* chose not to write.
*
* Which action shows is decided by the server's `review.canReply`, never by
* comparing the deadline against the browser clock: it is false both when a reply
* already exists and when the 30-day window has closed. Past the window the card
* simply carries no action — it reads as closed rather than as an error.
*
* Reply also needs the merchant's permission. It leads to the tab's composer,
* whose POST is routed on the product-update grant rather than the product/stat
* read that draws this card — so without the gate a read-only user would be
* invited to write a reply the API will refuse.
*/
const EventReviewCard = ({ review, canReply, onReply, onViewReply }: Props) => {
const date = formatReviewDate(review.createdAt);
return (
<Card data-testid={`event-review-${review.id}`}>
<Stack direction="row" gap="12px" alignItems="flex-start">
<GiveAvatar
size="32px"
imageUrl={review.reviewerAvatarURL}
name={review.reviewerName}
/>
<Stack gap="4px" flex={1} minWidth={0}>
<GiveText variant="bodyM" color="primary">
{review.reviewerName}
</GiveText>
<Stack direction="row" gap="8px" alignItems="center">
<GiveStarRating value={review.rating} />
{/* An unset date renders nothing rather than a stray separator. */}
{date && (
<GiveText variant="bodyXS" color="secondary">
· {date}
</GiveText>
)}
</Stack>
</Stack>
</Stack>
{/* No comment is a complete review, so the block is dropped entirely
rather than reserved and left blank. */}
{review.comment && (
<GiveText
variant="bodyS"
color="primary"
sx={{
display: "-webkit-box",
WebkitLineClamp: COMMENT_CLAMP_LINES,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{review.comment}
</GiveText>
)}
<Box sx={{ marginTop: "auto" }}>
{review.hasReply && (
<GiveLink
component="button"
color="primary"
Icon={ChatCircleIcon}
onClick={() => onViewReply?.(review)}
>
View your reply
</GiveLink>
)}
{review.canReply && canReply && onReply && (
<GiveLink
component="button"
color="primary"
onClick={() => onReply(review)}
>
Reply
</GiveLink>
)}
</Box>
</Card>
);
};
export default EventReviewCard;
const Card = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: "12px",
padding: "20px",
borderRadius: `${theme?.customs?.radius?.medium}px`,
backgroundColor: theme.palette.background.paper,
// The preview grid is two cards per row, so a fixed height keeps every card
// in a row ending at the same baseline whatever its comment length.
height: "190px",
}));
|