Remove the item page drawer, just link to the item page instead
The wardrobe-2020 app had a cute drawer that embeds the item page, but honestly I don't think it was that valuable, and especially not when it means we have to basically maintain two item pages lol. Let's decrease the surface area!
This commit is contained in:
parent
a2feee2d9b
commit
a18ffb22a7
5 changed files with 22 additions and 1213 deletions
|
@ -1,30 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerCloseButton,
|
|
||||||
DrawerOverlay,
|
|
||||||
useBreakpointValue,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { ItemPageContent } from "./ItemPage";
|
|
||||||
|
|
||||||
function ItemPageDrawer({ item, isOpen, onClose }) {
|
|
||||||
const placement = useBreakpointValue({ base: "bottom", lg: "right" });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer placement={placement} size="md" isOpen={isOpen} onClose={onClose}>
|
|
||||||
<DrawerOverlay>
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerCloseButton />
|
|
||||||
<DrawerBody>
|
|
||||||
<ItemPageContent itemId={item.id} isEmbedded />
|
|
||||||
</DrawerBody>
|
|
||||||
</DrawerContent>
|
|
||||||
</DrawerOverlay>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemPageDrawer;
|
|
|
@ -1,411 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Flex,
|
|
||||||
Popover,
|
|
||||||
PopoverArrow,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Portal,
|
|
||||||
Select,
|
|
||||||
Skeleton,
|
|
||||||
Spinner,
|
|
||||||
Tooltip,
|
|
||||||
useToast,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { ExternalLinkIcon, ChevronRightIcon } from "@chakra-ui/icons";
|
|
||||||
import { gql, useMutation } from "@apollo/client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ItemBadgeList,
|
|
||||||
ItemKindBadge,
|
|
||||||
ItemThumbnail,
|
|
||||||
} from "./components/ItemCard";
|
|
||||||
import { Heading1 } from "./util";
|
|
||||||
|
|
||||||
import useSupport from "./WardrobePage/support/useSupport";
|
|
||||||
|
|
||||||
function ItemPageLayout({ children, item, isEmbedded }) {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<ItemPageHeader item={item} isEmbedded={isEmbedded} />
|
|
||||||
<Box>{children}</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageHeader({ item, isEmbedded }) {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="flex-start"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<SubtleSkeleton isLoaded={item?.thumbnailUrl} marginRight="4">
|
|
||||||
<ItemThumbnail item={item} size="lg" isActive flex="0 0 auto" />
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<Box>
|
|
||||||
<SubtleSkeleton isLoaded={item?.name}>
|
|
||||||
<Heading1
|
|
||||||
lineHeight="1.1"
|
|
||||||
// Nudge down the size a bit in the embed case, to better fit the
|
|
||||||
// tighter layout!
|
|
||||||
size={isEmbedded ? "xl" : "2xl"}
|
|
||||||
>
|
|
||||||
{item?.name || "Item name here"}
|
|
||||||
</Heading1>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<ItemPageBadges item={item} isEmbedded={isEmbedded} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SubtleSkeleton hides the skeleton animation until a second has passed, and
|
|
||||||
* doesn't fade in the content if it loads near-instantly. This helps avoid
|
|
||||||
* flash-of-content stuff!
|
|
||||||
*
|
|
||||||
* For plain Skeletons, we often use <Delay><Skeleton /></Delay> instead. But
|
|
||||||
* that pattern doesn't work as well for wrapper skeletons where we're using
|
|
||||||
* placeholder content for layout: we don't want the delay if the content
|
|
||||||
* really _is_ present!
|
|
||||||
*/
|
|
||||||
export function SubtleSkeleton({ isLoaded, ...props }) {
|
|
||||||
const [shouldFadeIn, setShouldFadeIn] = React.useState(false);
|
|
||||||
const [shouldShowSkeleton, setShouldShowSkeleton] = React.useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const t = setTimeout(() => {
|
|
||||||
if (!isLoaded) {
|
|
||||||
setShouldFadeIn(true);
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const t = setTimeout(() => setShouldShowSkeleton(true), 500);
|
|
||||||
return () => clearTimeout(t);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Skeleton
|
|
||||||
fadeDuration={shouldFadeIn ? undefined : 0}
|
|
||||||
startColor={shouldShowSkeleton ? undefined : "transparent"}
|
|
||||||
endColor={shouldShowSkeleton ? undefined : "transparent"}
|
|
||||||
isLoaded={isLoaded}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageBadges({ item, isEmbedded }) {
|
|
||||||
const searchBadgesAreLoaded = item?.name != null && item?.isNc != null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemBadgeList marginTop="1">
|
|
||||||
<SubtleSkeleton isLoaded={item?.isNc != null}>
|
|
||||||
<ItemKindBadgeWithSupportTools item={item} />
|
|
||||||
</SubtleSkeleton>
|
|
||||||
{
|
|
||||||
// If the createdAt date is null (loaded and empty), hide the badge.
|
|
||||||
item?.createdAt !== null && (
|
|
||||||
<SubtleSkeleton
|
|
||||||
// Distinguish between undefined (still loading) and null (loaded and
|
|
||||||
// empty).
|
|
||||||
isLoaded={item?.createdAt !== undefined}
|
|
||||||
>
|
|
||||||
<Badge
|
|
||||||
display="block"
|
|
||||||
minWidth="5.25em"
|
|
||||||
boxSizing="content-box"
|
|
||||||
textAlign="center"
|
|
||||||
>
|
|
||||||
{item?.createdAt && <ShortTimestamp when={item?.createdAt} />}
|
|
||||||
</Badge>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
<LinkBadge
|
|
||||||
href={`https://impress.openneo.net/items/${item?.id}`}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Classic DTI
|
|
||||||
</LinkBadge>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
<LinkBadge
|
|
||||||
href={
|
|
||||||
"https://items.jellyneo.net/search/?name=" +
|
|
||||||
encodeURIComponent(item?.name) +
|
|
||||||
"&name_type=3"
|
|
||||||
}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Jellyneo
|
|
||||||
</LinkBadge>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
{item?.isNc && (
|
|
||||||
<SubtleSkeleton
|
|
||||||
isLoaded={
|
|
||||||
// Distinguish between undefined (still loading) and null (loaded
|
|
||||||
// and empty).
|
|
||||||
item?.ncTradeValueText !== undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item?.ncTradeValueText && (
|
|
||||||
<LinkBadge href="https://www.neopets.com/~owls">
|
|
||||||
OWLS: {item?.ncTradeValueText}
|
|
||||||
</LinkBadge>
|
|
||||||
)}
|
|
||||||
</SubtleSkeleton>
|
|
||||||
)}
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
{!item?.isNc && !item?.isPb && (
|
|
||||||
<LinkBadge
|
|
||||||
href={
|
|
||||||
"https://www.neopets.com/shops/wizard.phtml?string=" +
|
|
||||||
encodeURIComponent(item?.name)
|
|
||||||
}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Shop Wiz
|
|
||||||
</LinkBadge>
|
|
||||||
)}
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
{!item?.isNc && !item?.isPb && (
|
|
||||||
<LinkBadge
|
|
||||||
href={
|
|
||||||
"https://www.neopets.com/portal/supershopwiz.phtml?string=" +
|
|
||||||
encodeURIComponent(item?.name)
|
|
||||||
}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Super Wiz
|
|
||||||
</LinkBadge>
|
|
||||||
)}
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
{!item?.isNc && !item?.isPb && (
|
|
||||||
<LinkBadge
|
|
||||||
href={
|
|
||||||
"https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&search_string=" +
|
|
||||||
encodeURIComponent(item?.name)
|
|
||||||
}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Trade Post
|
|
||||||
</LinkBadge>
|
|
||||||
)}
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<SubtleSkeleton isLoaded={searchBadgesAreLoaded}>
|
|
||||||
{!item?.isNc && !item?.isPb && (
|
|
||||||
<LinkBadge
|
|
||||||
href={
|
|
||||||
"https://www.neopets.com/genie.phtml?type=process_genie&criteria=exact&auctiongenie=" +
|
|
||||||
encodeURIComponent(item?.name)
|
|
||||||
}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
>
|
|
||||||
Auctions
|
|
||||||
</LinkBadge>
|
|
||||||
)}
|
|
||||||
</SubtleSkeleton>
|
|
||||||
</ItemBadgeList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemKindBadgeWithSupportTools({ item }) {
|
|
||||||
const { isSupportUser, supportSecret } = useSupport();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const ncRef = React.useRef(null);
|
|
||||||
|
|
||||||
const isNcAutoDetectedFromRarity =
|
|
||||||
item?.rarityIndex === 500 || item?.rarityIndex === 0;
|
|
||||||
|
|
||||||
const [mutate, { loading }] = useMutation(gql`
|
|
||||||
mutation ItemPageSupportSetIsManuallyNc(
|
|
||||||
$itemId: ID!
|
|
||||||
$isManuallyNc: Boolean!
|
|
||||||
$supportSecret: String!
|
|
||||||
) {
|
|
||||||
setItemIsManuallyNc(
|
|
||||||
itemId: $itemId
|
|
||||||
isManuallyNc: $isManuallyNc
|
|
||||||
supportSecret: $supportSecret
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
isNc
|
|
||||||
isManuallyNc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (
|
|
||||||
isSupportUser &&
|
|
||||||
item?.rarityIndex != null &&
|
|
||||||
item?.isManuallyNc != null
|
|
||||||
) {
|
|
||||||
// TODO: Could code-split this into a SupportOnly file...
|
|
||||||
return (
|
|
||||||
<Popover placement="bottom-start" initialFocusRef={ncRef} showArrow>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<ItemKindBadge isNc={item.isNc} isPb={item.isPb} isEditButton />
|
|
||||||
</PopoverTrigger>
|
|
||||||
<Portal>
|
|
||||||
<PopoverContent padding="4">
|
|
||||||
<PopoverArrow />
|
|
||||||
<VStack spacing="2" align="flex-start">
|
|
||||||
<Flex align="center">
|
|
||||||
<Box as="span" fontWeight="600" marginRight="2">
|
|
||||||
NC:
|
|
||||||
</Box>
|
|
||||||
<Select
|
|
||||||
ref={ncRef}
|
|
||||||
size="xs"
|
|
||||||
value={item.isManuallyNc ? "true" : "false"}
|
|
||||||
onChange={(e) => {
|
|
||||||
const isManuallyNc = e.target.value === "true";
|
|
||||||
mutate({
|
|
||||||
variables: {
|
|
||||||
itemId: item.id,
|
|
||||||
isManuallyNc,
|
|
||||||
supportSecret,
|
|
||||||
},
|
|
||||||
optimisticResponse: {
|
|
||||||
setItemIsManuallyNc: {
|
|
||||||
__typename: "Item",
|
|
||||||
id: item.id,
|
|
||||||
isNc: isManuallyNc || isNcAutoDetectedFromRarity,
|
|
||||||
isManuallyNc,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
title:
|
|
||||||
"Could not set NC status for this item. Try again?",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="false">
|
|
||||||
Auto-detect: {isNcAutoDetectedFromRarity ? "Yes" : "No"}.{" "}
|
|
||||||
(Rarity {item.rarityIndex})
|
|
||||||
</option>
|
|
||||||
<option value="true">Manually set: Yes.</option>
|
|
||||||
</Select>
|
|
||||||
{loading && <Spinner size="sm" marginLeft="2" />}
|
|
||||||
</Flex>
|
|
||||||
<Flex align="center">
|
|
||||||
<Box as="span" fontWeight="600" marginRight="1">
|
|
||||||
PB:
|
|
||||||
</Box>
|
|
||||||
<Select size="xs" isReadOnly value="auto-detect">
|
|
||||||
<option value="auto-detect">
|
|
||||||
Auto-detect: {item.isPb ? "Yes" : "No"}. (from description)
|
|
||||||
</option>
|
|
||||||
<option style={{ fontStyle: "italic" }}>
|
|
||||||
(This cannot be manually set.)
|
|
||||||
</option>
|
|
||||||
</Select>
|
|
||||||
</Flex>
|
|
||||||
<Badge
|
|
||||||
colorScheme="pink"
|
|
||||||
alignSelf="flex-end"
|
|
||||||
marginBottom="-2"
|
|
||||||
marginRight="-2"
|
|
||||||
>
|
|
||||||
Support <span aria-hidden="true">💖</span>
|
|
||||||
</Badge>
|
|
||||||
</VStack>
|
|
||||||
</PopoverContent>
|
|
||||||
</Portal>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ItemKindBadge isNc={item?.isNc} isPb={item?.isPb} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkBadge = React.forwardRef(
|
|
||||||
({ children, href, isEmbedded, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
ref={ref}
|
|
||||||
as="a"
|
|
||||||
href={href}
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
// Normally we want to act like a normal webpage, and treat links as
|
|
||||||
// normal. But when we're on the wardrobe page, we want to avoid
|
|
||||||
// disrupting the outfit, and open in a new window instead.
|
|
||||||
target={isEmbedded ? "_blank" : undefined}
|
|
||||||
_focus={{ outline: "none", boxShadow: "outline" }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{
|
|
||||||
// We also change the icon to signal whether this will launch in a new
|
|
||||||
// window or not!
|
|
||||||
isEmbedded ? (
|
|
||||||
<ExternalLinkIcon marginLeft="1" />
|
|
||||||
) : (
|
|
||||||
<ChevronRightIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const fullDateFormatter = new Intl.DateTimeFormat("en-US", {
|
|
||||||
dateStyle: "long",
|
|
||||||
});
|
|
||||||
const monthYearFormatter = new Intl.DateTimeFormat("en-US", {
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
const monthDayYearFormatter = new Intl.DateTimeFormat("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
function ShortTimestamp({ when }) {
|
|
||||||
const date = new Date(when);
|
|
||||||
|
|
||||||
// To find the start of last month, take today, then set its date to the 1st
|
|
||||||
// and its time to midnight (the start of this month), and subtract one
|
|
||||||
// month. (JS handles negative months and rolls them over correctly.)
|
|
||||||
const startOfLastMonth = new Date();
|
|
||||||
startOfLastMonth.setDate(1);
|
|
||||||
startOfLastMonth.setHours(0);
|
|
||||||
startOfLastMonth.setMinutes(0);
|
|
||||||
startOfLastMonth.setSeconds(0);
|
|
||||||
startOfLastMonth.setMilliseconds(0);
|
|
||||||
startOfLastMonth.setMonth(startOfLastMonth.getMonth() - 1);
|
|
||||||
|
|
||||||
const dateIsOlderThanLastMonth = date < startOfLastMonth;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
label={`First seen on ${fullDateFormatter.format(date)}`}
|
|
||||||
placement="top"
|
|
||||||
openDelay={400}
|
|
||||||
>
|
|
||||||
{dateIsOlderThanLastMonth
|
|
||||||
? monthYearFormatter.format(date)
|
|
||||||
: monthDayYearFormatter.format(date)}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ItemPageLayout;
|
|
|
@ -1,771 +1,37 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ClassNames } from "@emotion/react";
|
import { useQuery } from "@apollo/client";
|
||||||
|
import gql from "graphql-tag";
|
||||||
import {
|
import {
|
||||||
AspectRatio,
|
AspectRatio,
|
||||||
Button,
|
|
||||||
Box,
|
Box,
|
||||||
HStack,
|
Button,
|
||||||
IconButton,
|
|
||||||
SkeletonText,
|
|
||||||
Tooltip,
|
|
||||||
VisuallyHidden,
|
|
||||||
VStack,
|
|
||||||
useBreakpointValue,
|
|
||||||
useColorModeValue,
|
|
||||||
useTheme,
|
|
||||||
useToast,
|
|
||||||
Flex,
|
Flex,
|
||||||
usePrefersReducedMotion,
|
|
||||||
Grid,
|
Grid,
|
||||||
Popover,
|
IconButton,
|
||||||
PopoverContent,
|
Tooltip,
|
||||||
PopoverTrigger,
|
useColorModeValue,
|
||||||
Checkbox,
|
usePrefersReducedMotion,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import {
|
import { EditIcon, WarningIcon } from "@chakra-ui/icons";
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
EditIcon,
|
|
||||||
StarIcon,
|
|
||||||
WarningIcon,
|
|
||||||
} from "@chakra-ui/icons";
|
|
||||||
import { MdPause, MdPlayArrow } from "react-icons/md";
|
import { MdPause, MdPlayArrow } from "react-icons/md";
|
||||||
import gql from "graphql-tag";
|
|
||||||
import { useQuery, useMutation } from "@apollo/client";
|
|
||||||
|
|
||||||
import ItemPageLayout, { SubtleSkeleton } from "./ItemPageLayout";
|
|
||||||
import {
|
|
||||||
Delay,
|
|
||||||
logAndCapture,
|
|
||||||
MajorErrorMessage,
|
|
||||||
useLocalStorage,
|
|
||||||
} from "./util";
|
|
||||||
import HTML5Badge, { layerUsesHTML5 } from "./components/HTML5Badge";
|
import HTML5Badge, { layerUsesHTML5 } from "./components/HTML5Badge";
|
||||||
import {
|
|
||||||
itemAppearanceFragment,
|
|
||||||
petAppearanceFragment,
|
|
||||||
} from "./components/useOutfitAppearance";
|
|
||||||
import { useOutfitPreview } from "./components/OutfitPreview";
|
|
||||||
import SpeciesColorPicker, {
|
import SpeciesColorPicker, {
|
||||||
useAllValidPetPoses,
|
useAllValidPetPoses,
|
||||||
getValidPoses,
|
getValidPoses,
|
||||||
getClosestPose,
|
getClosestPose,
|
||||||
} from "./components/SpeciesColorPicker";
|
} from "./components/SpeciesColorPicker";
|
||||||
import useCurrentUser from "./components/useCurrentUser";
|
|
||||||
import SpeciesFacesPicker, {
|
import SpeciesFacesPicker, {
|
||||||
colorIsBasic,
|
colorIsBasic,
|
||||||
} from "./ItemPage/SpeciesFacesPicker";
|
} from "./ItemPage/SpeciesFacesPicker";
|
||||||
|
import {
|
||||||
|
itemAppearanceFragment,
|
||||||
|
petAppearanceFragment,
|
||||||
|
} from "./components/useOutfitAppearance";
|
||||||
|
import { useOutfitPreview } from "./components/OutfitPreview";
|
||||||
|
import { logAndCapture, useLocalStorage } from "./util";
|
||||||
|
|
||||||
// Removed for the wardrobe-2020 case.
|
function ItemPageOutfitPreview({ itemId }) {
|
||||||
// TODO: Refactor this stuff, do we even need ItemPageContent really?
|
|
||||||
// function ItemPage() {
|
|
||||||
// const { query } = useRouter();
|
|
||||||
// return <ItemPageContent itemId={query.itemId} />;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ItemPageContent is the content of ItemPage, but we also use it as the
|
|
||||||
* entry point for ItemPageDrawer! When embedded in ItemPageDrawer, the
|
|
||||||
* `isEmbedded` prop is true, so we know not to e.g. set the page title.
|
|
||||||
*/
|
|
||||||
export function ItemPageContent({ itemId, isEmbedded = false }) {
|
|
||||||
const { isLoggedIn } = useCurrentUser();
|
|
||||||
|
|
||||||
const { error, data } = useQuery(
|
|
||||||
gql`
|
|
||||||
query ItemPage($itemId: ID!) {
|
|
||||||
item(id: $itemId) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isNc
|
|
||||||
isPb
|
|
||||||
thumbnailUrl
|
|
||||||
description
|
|
||||||
createdAt
|
|
||||||
ncTradeValueText
|
|
||||||
|
|
||||||
# For Support users.
|
|
||||||
rarityIndex
|
|
||||||
isManuallyNc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{ variables: { itemId }, returnPartialData: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <MajorErrorMessage error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = data?.item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ItemPageLayout item={item} isEmbedded={isEmbedded}>
|
|
||||||
<VStack spacing="8" marginTop="4">
|
|
||||||
<ItemPageDescription
|
|
||||||
description={item?.description}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
/>
|
|
||||||
<VStack spacing="4">
|
|
||||||
<ItemPageTradeLinks itemId={itemId} isEmbedded={isEmbedded} />
|
|
||||||
{isLoggedIn && <ItemPageOwnWantButtons itemId={itemId} />}
|
|
||||||
</VStack>
|
|
||||||
{!isEmbedded && <ItemPageOutfitPreview itemId={itemId} />}
|
|
||||||
</VStack>
|
|
||||||
</ItemPageLayout>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageDescription({ description, isEmbedded }) {
|
|
||||||
// Show 2 lines of description text placeholder on small screens, or when
|
|
||||||
// embedded in the wardrobe page's narrow drawer. In larger contexts, show
|
|
||||||
// just 1 line.
|
|
||||||
const viewportNumDescriptionLines = useBreakpointValue({ base: 2, md: 1 });
|
|
||||||
const numDescriptionLines = isEmbedded ? 2 : viewportNumDescriptionLines;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box width="100%" alignSelf="flex-start">
|
|
||||||
{description ? (
|
|
||||||
description
|
|
||||||
) : description === "" ? (
|
|
||||||
<i>(This item has no description.)</i>
|
|
||||||
) : (
|
|
||||||
<Box
|
|
||||||
maxWidth="40em"
|
|
||||||
minHeight={numDescriptionLines * 1.5 + "em"}
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
alignItems="stretch"
|
|
||||||
justifyContent="center"
|
|
||||||
>
|
|
||||||
<Delay ms={500}>
|
|
||||||
<SkeletonText noOfLines={numDescriptionLines} spacing="4" />
|
|
||||||
</Delay>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ITEM_PAGE_OWN_WANT_BUTTONS_QUERY = gql`
|
|
||||||
query ItemPageOwnWantButtons($itemId: ID!) {
|
|
||||||
item(id: $itemId) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
currentUserOwnsThis
|
|
||||||
currentUserWantsThis
|
|
||||||
}
|
|
||||||
currentUser {
|
|
||||||
closetLists {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isDefaultList
|
|
||||||
ownsOrWantsItems
|
|
||||||
hasItem(itemId: $itemId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function ItemPageOwnWantButtons({ itemId }) {
|
|
||||||
const { loading, error, data } = useQuery(ITEM_PAGE_OWN_WANT_BUTTONS_QUERY, {
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Box color="red.400">{error.message}</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closetLists = data?.currentUser?.closetLists || [];
|
|
||||||
const realLists = closetLists.filter((cl) => !cl.isDefaultList);
|
|
||||||
const ownedLists = realLists.filter((cl) => cl.ownsOrWantsItems === "OWNS");
|
|
||||||
const wantedLists = realLists.filter((cl) => cl.ownsOrWantsItems === "WANTS");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid
|
|
||||||
templateRows="auto auto"
|
|
||||||
templateColumns="160px 160px"
|
|
||||||
gridAutoFlow="column"
|
|
||||||
rowGap="0.5"
|
|
||||||
columnGap="4"
|
|
||||||
justifyItems="center"
|
|
||||||
>
|
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
|
||||||
<ItemPageOwnButton
|
|
||||||
itemId={itemId}
|
|
||||||
isChecked={data?.item?.currentUserOwnsThis}
|
|
||||||
/>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<ItemPageOwnWantListsDropdown
|
|
||||||
closetLists={ownedLists}
|
|
||||||
item={data?.item}
|
|
||||||
// Show the dropdown if the user owns this, and has at least one custom
|
|
||||||
// list it could belong to.
|
|
||||||
isVisible={data?.item?.currentUserOwnsThis && ownedLists.length >= 1}
|
|
||||||
popoverPlacement="bottom-end"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
|
||||||
<ItemPageWantButton
|
|
||||||
itemId={itemId}
|
|
||||||
isChecked={data?.item?.currentUserWantsThis}
|
|
||||||
/>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<ItemPageOwnWantListsDropdown
|
|
||||||
closetLists={wantedLists}
|
|
||||||
item={data?.item}
|
|
||||||
// Show the dropdown if the user wants this, and has at least one
|
|
||||||
// custom list it could belong to.
|
|
||||||
isVisible={data?.item?.currentUserWantsThis && wantedLists.length >= 1}
|
|
||||||
popoverPlacement="bottom-start"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageOwnWantListsDropdown({
|
|
||||||
closetLists,
|
|
||||||
item,
|
|
||||||
isVisible,
|
|
||||||
popoverPlacement,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Popover placement={popoverPlacement}>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<ItemPageOwnWantListsDropdownButton
|
|
||||||
closetLists={closetLists}
|
|
||||||
isVisible={isVisible}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent padding="2" width="64">
|
|
||||||
<ItemPageOwnWantListsDropdownContent
|
|
||||||
closetLists={closetLists}
|
|
||||||
item={item}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemPageOwnWantListsDropdownButton = React.forwardRef(
|
|
||||||
({ closetLists, isVisible, ...props }, ref) => {
|
|
||||||
const listsToShow = closetLists.filter((cl) => cl.hasItem);
|
|
||||||
|
|
||||||
let buttonText;
|
|
||||||
if (listsToShow.length === 1) {
|
|
||||||
buttonText = `In list: "${listsToShow[0].name}"`;
|
|
||||||
} else if (listsToShow.length > 1) {
|
|
||||||
const listNames = listsToShow.map((cl) => `"${cl.name}"`).join(", ");
|
|
||||||
buttonText = `${listsToShow.length} lists: ${listNames}`;
|
|
||||||
} else {
|
|
||||||
buttonText = "Add to list";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
ref={ref}
|
|
||||||
as="button"
|
|
||||||
fontSize="xs"
|
|
||||||
alignItems="center"
|
|
||||||
borderRadius="sm"
|
|
||||||
width="100%"
|
|
||||||
_hover={{ textDecoration: "underline" }}
|
|
||||||
_focus={{
|
|
||||||
textDecoration: "underline",
|
|
||||||
outline: "0",
|
|
||||||
boxShadow: "outline",
|
|
||||||
}}
|
|
||||||
// Even when the button isn't visible, we still render it for layout
|
|
||||||
// purposes, but hidden and disabled.
|
|
||||||
opacity={isVisible ? 1 : 0}
|
|
||||||
aria-hidden={!isVisible}
|
|
||||||
disabled={!isVisible}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{/* Flex tricks to center the text, ignoring the caret */}
|
|
||||||
<Box flex="1 0 0" />
|
|
||||||
<Box textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
|
||||||
{buttonText}
|
|
||||||
</Box>
|
|
||||||
<Flex flex="1 0 0">
|
|
||||||
<ChevronDownIcon marginLeft="1" />
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function ItemPageOwnWantListsDropdownContent({ closetLists, item }) {
|
|
||||||
return (
|
|
||||||
<Box as="ul" listStyleType="none">
|
|
||||||
{closetLists.map((closetList) => (
|
|
||||||
<Box key={closetList.id} as="li">
|
|
||||||
<ItemPageOwnWantsListsDropdownRow
|
|
||||||
closetList={closetList}
|
|
||||||
item={item}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageOwnWantsListsDropdownRow({ closetList, item }) {
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const [sendAddToListMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPage_AddToClosetList($listId: ID!, $itemId: ID!) {
|
|
||||||
addItemToClosetList(
|
|
||||||
listId: $listId
|
|
||||||
itemId: $itemId
|
|
||||||
removeFromDefaultList: true
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
hasItem(itemId: $itemId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{ context: { sendAuth: true } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const [sendRemoveFromListMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPage_RemoveFromClosetList($listId: ID!, $itemId: ID!) {
|
|
||||||
removeItemFromClosetList(
|
|
||||||
listId: $listId
|
|
||||||
itemId: $itemId
|
|
||||||
ensureInSomeList: true
|
|
||||||
) {
|
|
||||||
id
|
|
||||||
hasItem(itemId: $itemId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{ context: { sendAuth: true } },
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
|
||||||
(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
sendAddToListMutation({
|
|
||||||
variables: { listId: closetList.id, itemId: item.id },
|
|
||||||
optimisticResponse: {
|
|
||||||
addItemToClosetList: {
|
|
||||||
__typename: "ClosetList",
|
|
||||||
id: closetList.id,
|
|
||||||
hasItem: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
title: `Oops, error adding "${item.name}" to "${closetList.name}!"`,
|
|
||||||
description:
|
|
||||||
"Check your connection and try again? Sorry about this!",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sendRemoveFromListMutation({
|
|
||||||
variables: { listId: closetList.id, itemId: item.id },
|
|
||||||
optimisticResponse: {
|
|
||||||
removeItemFromClosetList: {
|
|
||||||
__typename: "ClosetList",
|
|
||||||
id: closetList.id,
|
|
||||||
hasItem: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast({
|
|
||||||
status: "error",
|
|
||||||
title: `Oops, error removing "${item.name}" from "${closetList.name}!"`,
|
|
||||||
description:
|
|
||||||
"Check your connection and try again? Sorry about this!",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
closetList,
|
|
||||||
item,
|
|
||||||
sendAddToListMutation,
|
|
||||||
sendRemoveFromListMutation,
|
|
||||||
toast,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
size="sm"
|
|
||||||
width="100%"
|
|
||||||
value={closetList.id}
|
|
||||||
isChecked={closetList.hasItem}
|
|
||||||
onChange={onChange}
|
|
||||||
>
|
|
||||||
{closetList.name}
|
|
||||||
</Checkbox>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageOwnButton({ itemId, isChecked }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const [sendAddMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPageOwnButtonAdd($itemId: ID!) {
|
|
||||||
addToItemsCurrentUserOwns(itemId: $itemId) {
|
|
||||||
id
|
|
||||||
currentUserOwnsThis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: "Mutation",
|
|
||||||
addToItemsCurrentUserOwns: {
|
|
||||||
__typename: "Item",
|
|
||||||
id: itemId,
|
|
||||||
currentUserOwnsThis: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Refactor the mutation result to include closet lists
|
|
||||||
refetchQueries: [
|
|
||||||
{
|
|
||||||
query: ITEM_PAGE_OWN_WANT_BUTTONS_QUERY,
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [sendRemoveMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPageOwnButtonRemove($itemId: ID!) {
|
|
||||||
removeFromItemsCurrentUserOwns(itemId: $itemId) {
|
|
||||||
id
|
|
||||||
currentUserOwnsThis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: "Mutation",
|
|
||||||
removeFromItemsCurrentUserOwns: {
|
|
||||||
__typename: "Item",
|
|
||||||
id: itemId,
|
|
||||||
currentUserOwnsThis: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Refactor the mutation result to include closet lists
|
|
||||||
refetchQueries: [
|
|
||||||
{
|
|
||||||
query: ITEM_PAGE_OWN_WANT_BUTTONS_QUERY,
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box as="label">
|
|
||||||
<VisuallyHidden
|
|
||||||
as="input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
sendAddMutation().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title: "We had trouble adding this to the items you own.",
|
|
||||||
description:
|
|
||||||
"Check your internet connection, and try again.",
|
|
||||||
status: "error",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sendRemoveMutation().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title:
|
|
||||||
"We had trouble removing this from the items you own.",
|
|
||||||
description:
|
|
||||||
"Check your internet connection, and try again.",
|
|
||||||
status: "error",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
as="div"
|
|
||||||
colorScheme={isChecked ? "green" : "gray"}
|
|
||||||
size="lg"
|
|
||||||
cursor="pointer"
|
|
||||||
transitionDuration="0.4s"
|
|
||||||
className={css`
|
|
||||||
input:focus + & {
|
|
||||||
box-shadow: ${theme.shadows.outline};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<IconCheckbox
|
|
||||||
icon={<CheckIcon />}
|
|
||||||
isChecked={isChecked}
|
|
||||||
marginRight="0.5em"
|
|
||||||
/>
|
|
||||||
I own this
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageWantButton({ itemId, isChecked }) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const [sendAddMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPageWantButtonAdd($itemId: ID!) {
|
|
||||||
addToItemsCurrentUserWants(itemId: $itemId) {
|
|
||||||
id
|
|
||||||
currentUserWantsThis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: "Mutation",
|
|
||||||
addToItemsCurrentUserWants: {
|
|
||||||
__typename: "Item",
|
|
||||||
id: itemId,
|
|
||||||
currentUserWantsThis: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Refactor the mutation result to include closet lists
|
|
||||||
refetchQueries: [
|
|
||||||
{
|
|
||||||
query: ITEM_PAGE_OWN_WANT_BUTTONS_QUERY,
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [sendRemoveMutation] = useMutation(
|
|
||||||
gql`
|
|
||||||
mutation ItemPageWantButtonRemove($itemId: ID!) {
|
|
||||||
removeFromItemsCurrentUserWants(itemId: $itemId) {
|
|
||||||
id
|
|
||||||
currentUserWantsThis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
optimisticResponse: {
|
|
||||||
__typename: "Mutation",
|
|
||||||
removeFromItemsCurrentUserWants: {
|
|
||||||
__typename: "Item",
|
|
||||||
id: itemId,
|
|
||||||
currentUserWantsThis: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Refactor the mutation result to include closet lists
|
|
||||||
refetchQueries: [
|
|
||||||
{
|
|
||||||
query: ITEM_PAGE_OWN_WANT_BUTTONS_QUERY,
|
|
||||||
variables: { itemId },
|
|
||||||
context: { sendAuth: true },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box as="label">
|
|
||||||
<VisuallyHidden
|
|
||||||
as="input"
|
|
||||||
type="checkbox"
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
sendAddMutation().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title: "We had trouble adding this to the items you want.",
|
|
||||||
description:
|
|
||||||
"Check your internet connection, and try again.",
|
|
||||||
status: "error",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sendRemoveMutation().catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title:
|
|
||||||
"We had trouble removing this from the items you want.",
|
|
||||||
description:
|
|
||||||
"Check your internet connection, and try again.",
|
|
||||||
status: "error",
|
|
||||||
duration: 5000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
as="div"
|
|
||||||
colorScheme={isChecked ? "blue" : "gray"}
|
|
||||||
size="lg"
|
|
||||||
cursor="pointer"
|
|
||||||
transitionDuration="0.4s"
|
|
||||||
className={css`
|
|
||||||
input:focus + & {
|
|
||||||
box-shadow: ${theme.shadows.outline};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<IconCheckbox
|
|
||||||
icon={<StarIcon />}
|
|
||||||
isChecked={isChecked}
|
|
||||||
marginRight="0.5em"
|
|
||||||
/>
|
|
||||||
I want this
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageTradeLinks({ itemId, isEmbedded }) {
|
|
||||||
const { data, loading, error } = useQuery(
|
|
||||||
gql`
|
|
||||||
query ItemPageTradeLinks($itemId: ID!) {
|
|
||||||
item(id: $itemId) {
|
|
||||||
id
|
|
||||||
numUsersOfferingThis
|
|
||||||
numUsersSeekingThis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{ variables: { itemId } },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Box color="red.400">{error.message}</Box>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack spacing="2">
|
|
||||||
<Box as="header" fontSize="sm" fontWeight="bold">
|
|
||||||
Trading:
|
|
||||||
</Box>
|
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
|
||||||
<ItemPageTradeLink
|
|
||||||
href={`/items/${itemId}/trades/offering`}
|
|
||||||
count={data?.item?.numUsersOfferingThis || 0}
|
|
||||||
label="offering"
|
|
||||||
colorScheme="green"
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
/>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
<SubtleSkeleton isLoaded={!loading}>
|
|
||||||
<ItemPageTradeLink
|
|
||||||
href={`/items/${itemId}/trades/seeking`}
|
|
||||||
count={data?.item?.numUsersSeekingThis || 0}
|
|
||||||
label="seeking"
|
|
||||||
colorScheme="blue"
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
/>
|
|
||||||
</SubtleSkeleton>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemPageTradeLink({ href, count, label, colorScheme, isEmbedded }) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
href={href}
|
|
||||||
target={isEmbedded ? "_blank" : undefined}
|
|
||||||
size="xs"
|
|
||||||
variant="outline"
|
|
||||||
colorScheme={colorScheme}
|
|
||||||
borderRadius="full"
|
|
||||||
paddingRight="1"
|
|
||||||
>
|
|
||||||
<Box display="grid" gridTemplateAreas="single-area">
|
|
||||||
<Box gridArea="single-area" display="flex" justifyContent="center">
|
|
||||||
{count} {label} <ChevronRightIcon minHeight="1.2em" />
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
gridArea="single-area"
|
|
||||||
display="flex"
|
|
||||||
justifyContent="center"
|
|
||||||
visibility="hidden"
|
|
||||||
>
|
|
||||||
888 offering <ChevronRightIcon minHeight="1.2em" />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function IconCheckbox({ icon, isChecked, ...props }) {
|
|
||||||
return (
|
|
||||||
<Box display="grid" gridTemplateAreas="the-same-area" {...props}>
|
|
||||||
<Box
|
|
||||||
gridArea="the-same-area"
|
|
||||||
width="1em"
|
|
||||||
height="1em"
|
|
||||||
border="2px solid currentColor"
|
|
||||||
borderRadius="md"
|
|
||||||
opacity={isChecked ? "0" : "0.75"}
|
|
||||||
transform={isChecked ? "scale(0.75)" : "none"}
|
|
||||||
transition="all 0.4s"
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
gridArea="the-same-area"
|
|
||||||
display="flex"
|
|
||||||
opacity={isChecked ? "1" : "0"}
|
|
||||||
transform={isChecked ? "none" : "scale(0.1)"}
|
|
||||||
transition="all 0.4s"
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ItemPageOutfitPreview({ itemId }) {
|
|
||||||
const idealPose = React.useMemo(
|
const idealPose = React.useMemo(
|
||||||
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
|
() => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"),
|
||||||
[],
|
[],
|
||||||
|
@ -1260,10 +526,7 @@ function PlayPauseButton({ isPaused, onClick }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemZonesInfo({
|
function ItemZonesInfo({ compatibleBodiesAndTheirZones, restrictedZones }) {
|
||||||
compatibleBodiesAndTheirZones,
|
|
||||||
restrictedZones,
|
|
||||||
}) {
|
|
||||||
// Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
|
// Reorganize the body-and-zones data, into zone-and-bodies data. Also, we're
|
||||||
// merging zones with the same label, because that's how user-facing zone UI
|
// merging zones with the same label, because that's how user-facing zone UI
|
||||||
// generally works!
|
// generally works!
|
||||||
|
@ -1434,3 +697,5 @@ function buildSortKeyForZoneLabelsAndTheirBodies({ zoneLabel, bodies }) {
|
||||||
|
|
||||||
return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`;
|
return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ItemPageOutfitPreview;
|
|
@ -24,7 +24,6 @@ import {
|
||||||
import SupportOnly from "./support/SupportOnly";
|
import SupportOnly from "./support/SupportOnly";
|
||||||
import useSupport from "./support/useSupport";
|
import useSupport from "./support/useSupport";
|
||||||
|
|
||||||
const LoadableItemPageDrawer = loadable(() => import("../ItemPageDrawer"));
|
|
||||||
const LoadableItemSupportDrawer = loadable(() =>
|
const LoadableItemSupportDrawer = loadable(() =>
|
||||||
import("./support/ItemSupportDrawer"),
|
import("./support/ItemSupportDrawer"),
|
||||||
);
|
);
|
||||||
|
@ -56,7 +55,6 @@ function Item({
|
||||||
onRemove,
|
onRemove,
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
}) {
|
}) {
|
||||||
const [infoDrawerIsOpen, setInfoDrawerIsOpen] = React.useState(false);
|
|
||||||
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -97,24 +95,10 @@ function Item({
|
||||||
icon={<InfoIcon />}
|
icon={<InfoIcon />}
|
||||||
label="More info"
|
label="More info"
|
||||||
to={`/items/${item.id}`}
|
to={`/items/${item.id}`}
|
||||||
onClick={(e) => {
|
target="_blank"
|
||||||
const willProbablyOpenInNewTab =
|
|
||||||
e.metaKey || e.shiftKey || e.altKey || e.ctrlKey;
|
|
||||||
if (willProbablyOpenInNewTab) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInfoDrawerIsOpen(true);
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
<LoadableItemPageDrawer
|
|
||||||
item={item}
|
|
||||||
isOpen={infoDrawerIsOpen}
|
|
||||||
onClose={() => setInfoDrawerIsOpen(false)}
|
|
||||||
/>
|
|
||||||
<SupportOnly>
|
<SupportOnly>
|
||||||
<LoadableItemSupportDrawer
|
<LoadableItemSupportDrawer
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -232,7 +216,7 @@ function ItemBadges({ item }) {
|
||||||
/**
|
/**
|
||||||
* ItemActionButton is one of a list of actions a user can take for this item.
|
* ItemActionButton is one of a list of actions a user can take for this item.
|
||||||
*/
|
*/
|
||||||
function ItemActionButton({ icon, label, to, onClick }) {
|
function ItemActionButton({ icon, label, to, onClick, ...props }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const focusBackgroundColor = useColorModeValue(
|
const focusBackgroundColor = useColorModeValue(
|
||||||
|
@ -249,6 +233,7 @@ function ItemActionButton({ icon, label, to, onClick }) {
|
||||||
{({ css }) => (
|
{({ css }) => (
|
||||||
<Tooltip label={label} placement="top">
|
<Tooltip label={label} placement="top">
|
||||||
<LinkOrButton
|
<LinkOrButton
|
||||||
|
{...props}
|
||||||
component={IconButton}
|
component={IconButton}
|
||||||
href={to}
|
href={to}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import AppProvider from "./AppProvider";
|
import AppProvider from "./AppProvider";
|
||||||
import { ItemPageOutfitPreview } from "./ItemPage";
|
import ItemPageOutfitPreview from "./ItemPageOutfitPreview";
|
||||||
import WardrobePage from "./WardrobePage";
|
import WardrobePage from "./WardrobePage";
|
||||||
|
|
||||||
export { AppProvider, ItemPageOutfitPreview, WardrobePage };
|
export { AppProvider, ItemPageOutfitPreview, WardrobePage };
|
||||||
|
|
Loading…
Reference in a new issue