Emi Matchu
0e314482f7
I haven't been running Prettier consistently on things in this project. Now, it's quick-runnable, and I've got it on everything! Also, I just think tabs are the right default for this kind of thing, and I'm glad to get to switch over to it! (In `package.json`.)
430 lines
9.1 KiB
JavaScript
430 lines
9.1 KiB
JavaScript
import React from "react";
|
|
import { ClassNames } from "@emotion/react";
|
|
import {
|
|
Badge,
|
|
Box,
|
|
SimpleGrid,
|
|
Tooltip,
|
|
Wrap,
|
|
WrapItem,
|
|
useColorModeValue,
|
|
useTheme,
|
|
} from "@chakra-ui/react";
|
|
import {
|
|
CheckIcon,
|
|
EditIcon,
|
|
NotAllowedIcon,
|
|
StarIcon,
|
|
} from "@chakra-ui/icons";
|
|
import { HiSparkles } from "react-icons/hi";
|
|
|
|
import SquareItemCard from "./SquareItemCard";
|
|
import { safeImageUrl, useCommonStyles } from "../util";
|
|
import usePreferArchive from "./usePreferArchive";
|
|
|
|
function ItemCard({ item, badges, variant = "list", ...props }) {
|
|
const { brightBackground } = useCommonStyles();
|
|
|
|
switch (variant) {
|
|
case "grid":
|
|
return <SquareItemCard item={item} {...props} />;
|
|
case "list":
|
|
return (
|
|
<Box
|
|
as="a"
|
|
href={`/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({
|
|
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>
|
|
</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.
|
|
*/
|
|
export function ItemThumbnail({
|
|
item,
|
|
size = "md",
|
|
isActive,
|
|
isDisabled,
|
|
focusSelector,
|
|
...props
|
|
}) {
|
|
const [preferArchive] = usePreferArchive();
|
|
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)",
|
|
},
|
|
!isDisabled &&
|
|
!isActive && {
|
|
[focusSelector]: {
|
|
opacity: "0.9",
|
|
transform: "scale(0.9)",
|
|
},
|
|
},
|
|
!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`,
|
|
},
|
|
},
|
|
])}
|
|
>
|
|
{/* If the item is still loading, wait with an empty box. */}
|
|
{item && (
|
|
<Box
|
|
as="img"
|
|
width="100%"
|
|
height="100%"
|
|
src={safeImageUrl(item.thumbnailUrl, { preferArchive })}
|
|
alt={`Thumbnail art for ${item.name}`}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
</ClassNames>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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};
|
|
}
|
|
|
|
input:checked + .item-container & {
|
|
opacity: 1;
|
|
font-weight: ${theme.fontWeights.bold};
|
|
}
|
|
`
|
|
}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Box>
|
|
)}
|
|
</ClassNames>
|
|
);
|
|
}
|
|
|
|
export function ItemCardList({ children }) {
|
|
return (
|
|
<SimpleGrid columns={{ sm: 1, md: 2, lg: 3 }} spacing="6">
|
|
{children}
|
|
</SimpleGrid>
|
|
);
|
|
}
|
|
|
|
export function ItemBadgeList({ children, ...props }) {
|
|
return (
|
|
<Wrap spacing="2" opacity="0.7" {...props}>
|
|
{React.Children.map(
|
|
children,
|
|
(badge) => badge && <WrapItem>{badge}</WrapItem>,
|
|
)}
|
|
</Wrap>
|
|
);
|
|
}
|
|
|
|
export function ItemBadgeTooltip({ label, children }) {
|
|
return (
|
|
<Tooltip
|
|
label={<Box textAlign="center">{label}</Box>}
|
|
placement="top"
|
|
openDelay={400}
|
|
>
|
|
{children}
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
export const NcBadge = React.forwardRef(({ isEditButton, ...props }, ref) => {
|
|
return (
|
|
<ItemBadgeTooltip label="Neocash">
|
|
<Badge
|
|
ref={ref}
|
|
as={isEditButton ? "button" : "span"}
|
|
colorScheme="purple"
|
|
display="flex"
|
|
alignItems="center"
|
|
_focus={{ outline: "none", boxShadow: "outline" }}
|
|
{...props}
|
|
>
|
|
NC
|
|
{isEditButton && <EditIcon fontSize="0.85em" marginLeft="1" />}
|
|
</Badge>
|
|
</ItemBadgeTooltip>
|
|
);
|
|
});
|
|
|
|
export const NpBadge = React.forwardRef(({ isEditButton, ...props }, ref) => {
|
|
return (
|
|
<ItemBadgeTooltip label="Neopoints">
|
|
<Badge
|
|
ref={ref}
|
|
as={isEditButton ? "button" : "span"}
|
|
display="flex"
|
|
alignItems="center"
|
|
_focus={{ outline: "none", boxShadow: "outline" }}
|
|
{...props}
|
|
>
|
|
NP
|
|
{isEditButton && <EditIcon fontSize="0.85em" marginLeft="1" />}
|
|
</Badge>
|
|
</ItemBadgeTooltip>
|
|
);
|
|
});
|
|
|
|
export const PbBadge = React.forwardRef(({ isEditButton, ...props }, ref) => {
|
|
return (
|
|
<ItemBadgeTooltip label="This item is only obtainable via paintbrush">
|
|
<Badge
|
|
ref={ref}
|
|
as={isEditButton ? "button" : "span"}
|
|
colorScheme="orange"
|
|
display="flex"
|
|
alignItems="center"
|
|
_focus={{ outline: "none", boxShadow: "outline" }}
|
|
{...props}
|
|
>
|
|
PB
|
|
{isEditButton && <EditIcon fontSize="0.85em" marginLeft="1" />}
|
|
</Badge>
|
|
</ItemBadgeTooltip>
|
|
);
|
|
});
|
|
|
|
export const ItemKindBadge = React.forwardRef(
|
|
({ isNc, isPb, isEditButton, ...props }, ref) => {
|
|
if (isNc) {
|
|
return <NcBadge ref={ref} isEditButton={isEditButton} {...props} />;
|
|
} else if (isPb) {
|
|
return <PbBadge ref={ref} isEditButton={isEditButton} {...props} />;
|
|
} else {
|
|
return <NpBadge ref={ref} isEditButton={isEditButton} {...props} />;
|
|
}
|
|
},
|
|
);
|
|
|
|
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;
|