impress-2020/src/app/components/SquareItemCard.js
Matchu a0be9942fb Add extra flair to trade matches
I refactor the hide-badge thing into 3 "trade matching modes". Then, the logic for whether to hide specific badges moves into the component, and we use that same flag to decide whether to show the big word "match"!
2021-06-08 01:23:31 -07:00

344 lines
9.7 KiB
JavaScript

import React from "react";
import {
Skeleton,
useColorModeValue,
useTheme,
useToken,
} from "@chakra-ui/react";
import { ClassNames } from "@emotion/react";
import { Link } from "react-router-dom";
import { safeImageUrl, useCommonStyles } from "../util";
import { CheckIcon, StarIcon } from "@chakra-ui/icons";
function SquareItemCard({ item, tradeMatchingMode = null, ...props }) {
const outlineShadowValue = useToken("shadows", "outline");
return (
<ClassNames>
{({ css }) => (
// SquareItemCard renders in large lists of 1k+ items, so we get a big
// perf win by using Emotion directly instead of Chakra's styled-system
// Box.
<Link
to={`/items/${item.id}`}
className={css`
transition: all 0.2s;
&:hover,
&:focus {
transform: scale(1.05);
}
&:focus {
box-shadow: ${outlineShadowValue};
outline: none;
}
`}
{...props}
>
<SquareItemCardLayout
name={item.name}
thumbnailImage={
<ItemThumbnail
item={item}
tradeMatchingMode={tradeMatchingMode}
/>
}
/>
</Link>
)}
</ClassNames>
);
}
function SquareItemCardLayout({ name, thumbnailImage, minHeightNumLines = 2 }) {
const { brightBackground } = useCommonStyles();
const brightBackgroundValue = useToken("colors", brightBackground);
const theme = useTheme();
return (
// SquareItemCard renders in large lists of 1k+ items, so we get a big perf
// win by using Emotion directly instead of Chakra's styled-system Box.
<ClassNames>
{({ css }) => (
<div
className={css`
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: ${theme.shadows.md};
border-radius: ${theme.radii.md};
padding: ${theme.space["3"]};
width: calc(80px + 2em);
background: ${brightBackgroundValue};
`}
>
{thumbnailImage}
<div
className={css`
margin-top: ${theme.space["1"]};
font-size: ${theme.fontSizes.sm};
/* Set min height to match a 2-line item name, so the cards
* in a row aren't toooo differently sized... */
min-height: ${minHeightNumLines * 1.5 + "em"};
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
`}
// HACK: Emotion turns this into -webkit-display: -webkit-box?
style={{ display: "-webkit-box" }}
>
{name}
</div>
</div>
)}
</ClassNames>
);
}
function ItemThumbnail({ item, tradeMatchingMode }) {
const kindColorScheme = item.isNc ? "purple" : item.isPb ? "orange" : "gray";
const thumbnailShadowColor = useColorModeValue(
`${kindColorScheme}.200`,
`${kindColorScheme}.600`
);
const thumbnailShadowColorValue = useToken("colors", thumbnailShadowColor);
const mdRadiusValue = useToken("radii", "md");
// Normally, we just show the owns/wants badges depending on whether the
// current user owns/wants it. But, in a trade list, we use trade-matching
// mode instead: only show the badge if it represents a viable trade, and add
// some extra flair to it, too!
let showOwnsBadge;
let showWantsBadge;
let showTradeMatchFlair;
if (tradeMatchingMode == null) {
showOwnsBadge = item.currentUserOwnsThis;
showWantsBadge = item.currentUserWantsThis;
showTradeMatchFlair = false;
} else if (tradeMatchingMode === "offering") {
showOwnsBadge = false;
showWantsBadge = item.currentUserWantsThis;
showTradeMatchFlair = true;
} else if (tradeMatchingMode === "seeking") {
showOwnsBadge = item.currentUserOwnsThis;
showWantsBadge = false;
showTradeMatchFlair = true;
} else if (tradeMatchingMode === "hide-all") {
showOwnsBadge = false;
showWantsBadge = false;
showTradeMatchFlair = false;
} else {
throw new Error(`unexpected tradeMatchingMode ${tradeMatchingMode}`);
}
return (
<ClassNames>
{({ css }) => (
<div
className={css`
position: relative;
`}
>
<img
src={safeImageUrl(item.thumbnailUrl)}
alt={`Thumbnail art for ${item.name}`}
width={80}
height={80}
className={css`
border-radius: ${mdRadiusValue};
box-shadow: 0 0 4px ${thumbnailShadowColorValue};
/* Don't let alt text flash in while loading */
&:-moz-loading {
visibility: hidden;
}
`}
loading="lazy"
/>
<div
className={css`
position: absolute;
top: -6px;
left: -6px;
display: flex;
flex-direction: column;
gap: 2px;
`}
>
{showOwnsBadge && (
<ItemOwnsWantsBadge
colorScheme="green"
label={
showTradeMatchFlair
? "You own this, and they want it!"
: "You own this"
}
>
<CheckIcon />
{showTradeMatchFlair && (
<div
className={css`
margin-left: 0.25em;
margin-right: 0.125rem;
`}
>
Match
</div>
)}
</ItemOwnsWantsBadge>
)}
{showWantsBadge && (
<ItemOwnsWantsBadge
colorScheme="blue"
label={
showTradeMatchFlair
? "You want this, and they own it!"
: "You want this"
}
>
<StarIcon />
{showTradeMatchFlair && (
<div
className={css`
margin-left: 0.25em;
margin-right: 0.125rem;
`}
>
Match
</div>
)}
</ItemOwnsWantsBadge>
)}
</div>
{item.isNc != null && (
<div
className={css`
position: absolute;
bottom: -6px;
right: -3px;
`}
>
<ItemThumbnailKindBadge colorScheme={kindColorScheme}>
{item.isNc ? "NC" : item.isPb ? "PB" : "NP"}
</ItemThumbnailKindBadge>
</div>
)}
</div>
)}
</ClassNames>
);
}
function ItemOwnsWantsBadge({ colorScheme, children, label }) {
const badgeBackground = useColorModeValue(
`${colorScheme}.100`,
`${colorScheme}.500`
);
const badgeColor = useColorModeValue(
`${colorScheme}.500`,
`${colorScheme}.100`
);
const [badgeBackgroundValue, badgeColorValue] = useToken("colors", [
badgeBackground,
badgeColor,
]);
return (
<ClassNames>
{({ css }) => (
<div
aria-label={label}
title={label}
className={css`
border-radius: 999px;
height: 16px;
min-width: 16px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 2px ${badgeBackgroundValue};
/* Decrease the padding: I don't want to hit the edges, but I want
* to be a circle when possible! */
padding-left: 0.125rem;
padding-right: 0.125rem;
/* Copied from Chakra <Badge> */
white-space: nowrap;
vertical-align: middle;
text-transform: uppercase;
font-size: 0.65rem;
font-weight: 700;
background: ${badgeBackgroundValue};
color: ${badgeColorValue};
`}
>
{children}
</div>
)}
</ClassNames>
);
}
function ItemThumbnailKindBadge({ colorScheme, children }) {
const badgeBackground = useColorModeValue(
`${colorScheme}.100`,
`${colorScheme}.500`
);
const badgeColor = useColorModeValue(
`${colorScheme}.500`,
`${colorScheme}.100`
);
const [badgeBackgroundValue, badgeColorValue] = useToken("colors", [
badgeBackground,
badgeColor,
]);
return (
<ClassNames>
{({ css }) => (
<div
className={css`
/* Copied from Chakra <Badge> */
white-space: nowrap;
vertical-align: middle;
padding-left: 0.25rem;
padding-right: 0.25rem;
text-transform: uppercase;
font-size: 0.65rem;
border-radius: 0.125rem;
font-weight: 700;
background: ${badgeBackgroundValue};
color: ${badgeColorValue};
`}
>
{children}
</div>
)}
</ClassNames>
);
}
export function SquareItemCardSkeleton({ minHeightNumLines }) {
return (
<SquareItemCardLayout
name={
<>
<Skeleton width="100%" height="1em" marginTop="2" />
{minHeightNumLines >= 3 && (
<Skeleton width="100%" height="1em" marginTop="2" />
)}
</>
}
thumbnailImage={<Skeleton width="80px" height="80px" />}
minHeightNumLines={minHeightNumLines}
/>
);
}
export default SquareItemCard;