forked from OpenNeo/impress-2020
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"!
344 lines
9.7 KiB
JavaScript
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;
|