impress-2020/src/app/components/ItemCard.js

392 lines
8.9 KiB
JavaScript
Raw Normal View History

2020-09-06 23:49:04 -07:00
import React from "react";
import { ClassNames } from "@emotion/react";
import {
Badge,
Box,
SimpleGrid,
Tooltip,
Wrap,
2020-12-25 09:17:24 -08:00
WrapItem,
useColorModeValue,
useTheme,
2020-12-25 09:08:33 -08:00
} from "@chakra-ui/react";
import { CheckIcon, NotAllowedIcon, StarIcon } from "@chakra-ui/icons";
import { HiSparkles } from "react-icons/hi";
import { Link } from "react-router-dom";
2020-09-06 23:49:04 -07:00
2021-01-18 06:31:27 -08:00
import SquareItemCard from "./SquareItemCard";
import { safeImageUrl, useCommonStyles } from "../util";
2020-09-06 23:49:04 -07:00
function ItemCard({ item, badges, variant = "list", ...props }) {
const { brightBackground } = useCommonStyles();
switch (variant) {
case "grid":
2021-01-18 06:31:27 -08:00
return <SquareItemCard item={item} {...props} />;
case "list":
return (
<Box
as={Link}
to={`/items/${item.id}`}
display="block"
p="2"
boxShadow="lg"
borderRadius="lg"
background={brightBackground}
transition="all 0.2s"
className="item-card"
width="100%"
minWidth="0"
{...props}
>
<ItemCardContent
item={item}
badges={badges}
focusSelector=".item-card:hover &, .item-card:focus &"
/>
</Box>
);
default:
throw new Error(`Unexpected ItemCard variant: ${variant}`);
}
}
export function ItemCardContent({
2020-09-06 23:49:04 -07:00
item,
badges,
isWorn,
isDisabled,
itemNameId,
focusSelector,
}) {
return (
<Box display="flex">
<Box>
<Box flex="0 0 auto" marginRight="3">
<ItemThumbnail
item={item}
isActive={isWorn}
isDisabled={isDisabled}
focusSelector={focusSelector}
/>
</Box>
2020-09-06 23:49:04 -07:00
</Box>
<Box flex="1 1 0" minWidth="0" marginTop="1px">
<ItemName
id={itemNameId}
isWorn={isWorn}
isDisabled={isDisabled}
focusSelector={focusSelector}
>
{item.name}
</ItemName>
{badges}
</Box>
</Box>
);
}
/**
* ItemThumbnail shows a small preview image for the item, including some
* hover/focus and worn/unworn states.
*/
2020-09-11 23:56:47 -07:00
export function ItemThumbnail({
item,
size = "md",
isActive,
isDisabled,
focusSelector,
...props
}) {
2020-09-06 23:49:04 -07:00
const theme = useTheme();
const borderColor = useColorModeValue(
theme.colors.green["700"],
"transparent"
);
const focusBorderColor = useColorModeValue(
theme.colors.green["600"],
"transparent"
);
return (
<ClassNames>
{({ css }) => (
<Box
width={size === "lg" ? "80px" : "50px"}
height={size === "lg" ? "80px" : "50px"}
transition="all 0.15s"
transformOrigin="center"
position="relative"
className={css([
{
transform: "scale(0.8)",
2020-09-06 23:49:04 -07:00
},
!isDisabled &&
!isActive && {
[focusSelector]: {
opacity: "0.9",
transform: "scale(0.9)",
},
2020-09-06 23:49:04 -07:00
},
!isDisabled &&
isActive && {
opacity: 1,
transform: "none",
},
])}
{...props}
>
<Box
borderRadius="lg"
boxShadow="md"
border="1px"
overflow="hidden"
width="100%"
height="100%"
className={css([
{
borderColor: `${borderColor} !important`,
},
!isDisabled &&
!isActive && {
[focusSelector]: {
borderColor: `${focusBorderColor} !important`,
},
},
])}
>
<Box
as="img"
width="100%"
height="100%"
src={safeImageUrl(item.thumbnailUrl)}
alt={`Thumbnail art for ${item.name}`}
/>
</Box>
</Box>
)}
</ClassNames>
2020-09-06 23:49:04 -07:00
);
}
/**
* ItemName shows the item's name, including some hover/focus and worn/unworn
* states.
*/
function ItemName({ children, isDisabled, focusSelector, ...props }) {
const theme = useTheme();
return (
<ClassNames>
{({ css }) => (
<Box
fontSize="md"
transition="all 0.15s"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
className={
!isDisabled &&
css`
${focusSelector} {
opacity: 0.9;
font-weight: ${theme.fontWeights.medium};
}
2020-09-06 23:49:04 -07:00
input:checked + .item-container & {
opacity: 1;
font-weight: ${theme.fontWeights.bold};
}
`
2020-09-06 23:49:04 -07:00
}
{...props}
>
{children}
</Box>
)}
</ClassNames>
2020-09-06 23:49:04 -07:00
);
}
export function ItemCardList({ children }) {
return (
<SimpleGrid columns={{ sm: 1, md: 2, lg: 3 }} spacing="6">
{children}
</SimpleGrid>
);
}
export function ItemBadgeList({ children, ...props }) {
2020-09-06 23:49:04 -07:00
return (
<Wrap spacing="2" opacity="0.7" {...props}>
2020-12-25 09:17:24 -08:00
{React.Children.map(
children,
(badge) => badge && <WrapItem>{badge}</WrapItem>
)}
</Wrap>
2020-09-06 23:49:04 -07:00
);
}
export function ItemBadgeTooltip({ label, children }) {
return (
<Tooltip
label={<Box textAlign="center">{label}</Box>}
placement="top"
openDelay={400}
>
{children}
</Tooltip>
);
}
export function NcBadge() {
return (
<ItemBadgeTooltip label="Neocash">
<Badge colorScheme="purple" display="block">
NC
</Badge>
</ItemBadgeTooltip>
);
}
export function NpBadge() {
return (
<ItemBadgeTooltip label="Neopoints">
<Badge display="block">NP</Badge>
</ItemBadgeTooltip>
);
}
export function PbBadge() {
return (
<ItemBadgeTooltip label="This item is only obtainable via paintbrush">
<Badge colorScheme="orange" display="block">
PB
</Badge>
</ItemBadgeTooltip>
);
}
export function ItemKindBadge({ isNc, isPb }) {
if (isNc) {
return <NcBadge />;
} else if (isPb) {
return <PbBadge />;
} else {
return <NpBadge />;
}
}
export function YouOwnThisBadge({ variant = "long" }) {
let badge = (
<Badge
colorScheme="green"
display="flex"
alignItems="center"
minHeight="1.5em"
>
<CheckIcon aria-label="Check" />
{variant === "medium" && <Box marginLeft="1">Own</Box>}
{variant === "long" && <Box marginLeft="1">You own this!</Box>}
</Badge>
);
if (variant === "short" || variant === "medium") {
badge = (
<ItemBadgeTooltip label="You own this item">{badge}</ItemBadgeTooltip>
);
}
return badge;
}
export function YouWantThisBadge({ variant = "long" }) {
let badge = (
<Badge
colorScheme="blue"
display="flex"
alignItems="center"
minHeight="1.5em"
>
<StarIcon aria-label="Star" />
{variant === "medium" && <Box marginLeft="1">Want</Box>}
{variant === "long" && <Box marginLeft="1">You want this!</Box>}
</Badge>
);
if (variant === "short" || variant === "medium") {
badge = (
<ItemBadgeTooltip label="You want this item">{badge}</ItemBadgeTooltip>
);
}
return badge;
}
function ZoneBadge({ variant, zoneLabel }) {
// Shorten the label when necessary, to make the badges less bulky
const shorthand = zoneLabel
.replace("Background Item", "BG Item")
.replace("Foreground Item", "FG Item")
.replace("Lower-body", "Lower")
.replace("Upper-body", "Upper")
.replace("Transient", "Trans")
.replace("Biology", "Bio");
if (variant === "restricts") {
return (
<ItemBadgeTooltip
label={`Restricted: This item can't be worn with ${zoneLabel} items`}
>
<Badge>
<Box display="flex" alignItems="center">
{shorthand} <NotAllowedIcon marginLeft="1" />
</Box>
</Badge>
</ItemBadgeTooltip>
);
}
if (shorthand !== zoneLabel) {
return (
<ItemBadgeTooltip label={zoneLabel}>
<Badge>{shorthand}</Badge>
</ItemBadgeTooltip>
);
}
return <Badge>{shorthand}</Badge>;
}
export function getZoneBadges(zones, propsForAllBadges) {
// Get the sorted zone labels. Sometimes an item occupies multiple zones of
// the same name, so it's important to de-duplicate them!
let labels = zones.map((z) => z.label);
labels = new Set(labels);
labels = [...labels].sort();
return labels.map((label) => (
<ZoneBadge key={label} zoneLabel={label} {...propsForAllBadges} />
));
}
export function MaybeAnimatedBadge() {
return (
<ItemBadgeTooltip label="Maybe animated? (Support only)">
<Badge
colorScheme="orange"
display="flex"
alignItems="center"
minHeight="1.5em"
>
<Box as={HiSparkles} aria-label="Sparkles" />
</Badge>
</ItemBadgeTooltip>
);
}
export default ItemCard;