diff --git a/app/javascript/wardrobe-2020/ItemPageDrawer.js b/app/javascript/wardrobe-2020/ItemPageDrawer.js deleted file mode 100644 index 65426e8d..00000000 --- a/app/javascript/wardrobe-2020/ItemPageDrawer.js +++ /dev/null @@ -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 ( - - - - - - - - - - - ); -} - -export default ItemPageDrawer; diff --git a/app/javascript/wardrobe-2020/ItemPageLayout.js b/app/javascript/wardrobe-2020/ItemPageLayout.js deleted file mode 100644 index 38d7203e..00000000 --- a/app/javascript/wardrobe-2020/ItemPageLayout.js +++ /dev/null @@ -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 ( - - - {children} - - ); -} - -function ItemPageHeader({ item, isEmbedded }) { - return ( - - - - - - - - {item?.name || "Item name here"} - - - - - - ); -} - -/** - * 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 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 ( - - ); -} - -function ItemPageBadges({ item, isEmbedded }) { - const searchBadgesAreLoaded = item?.name != null && item?.isNc != null; - - return ( - - - - - { - // If the createdAt date is null (loaded and empty), hide the badge. - item?.createdAt !== null && ( - - - {item?.createdAt && } - - - ) - } - - - Classic DTI - - - - - Jellyneo - - - {item?.isNc && ( - - {item?.ncTradeValueText && ( - - OWLS: {item?.ncTradeValueText} - - )} - - )} - - {!item?.isNc && !item?.isPb && ( - - Trade Post - - )} - - - {!item?.isNc && !item?.isPb && ( - - Auctions - - )} - - - ); -} - -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 ( - - - - - - - - - - - NC: - - - {loading && } - - - - PB: - - - - - Support - - - - - - ); - } - - return ; -} - -const LinkBadge = React.forwardRef( - ({ children, href, isEmbedded, ...props }, ref) => { - return ( - - {children} - { - // We also change the icon to signal whether this will launch in a new - // window or not! - isEmbedded ? ( - - ) : ( - - ) - } - - ); - }, -); - -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 ( - - {dateIsOlderThanLastMonth - ? monthYearFormatter.format(date) - : monthDayYearFormatter.format(date)} - - ); -} - -export default ItemPageLayout; diff --git a/app/javascript/wardrobe-2020/ItemPage.js b/app/javascript/wardrobe-2020/ItemPageOutfitPreview.js similarity index 53% rename from app/javascript/wardrobe-2020/ItemPage.js rename to app/javascript/wardrobe-2020/ItemPageOutfitPreview.js index 7622fd2c..5e83ea20 100644 --- a/app/javascript/wardrobe-2020/ItemPage.js +++ b/app/javascript/wardrobe-2020/ItemPageOutfitPreview.js @@ -1,771 +1,37 @@ import React from "react"; -import { ClassNames } from "@emotion/react"; +import { useQuery } from "@apollo/client"; +import gql from "graphql-tag"; import { AspectRatio, - Button, Box, - HStack, - IconButton, - SkeletonText, - Tooltip, - VisuallyHidden, - VStack, - useBreakpointValue, - useColorModeValue, - useTheme, - useToast, + Button, Flex, - usePrefersReducedMotion, Grid, - Popover, - PopoverContent, - PopoverTrigger, - Checkbox, + IconButton, + Tooltip, + useColorModeValue, + usePrefersReducedMotion, } from "@chakra-ui/react"; -import { - CheckIcon, - ChevronDownIcon, - ChevronRightIcon, - EditIcon, - StarIcon, - WarningIcon, -} from "@chakra-ui/icons"; +import { EditIcon, WarningIcon } from "@chakra-ui/icons"; 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 { - itemAppearanceFragment, - petAppearanceFragment, -} from "./components/useOutfitAppearance"; -import { useOutfitPreview } from "./components/OutfitPreview"; import SpeciesColorPicker, { useAllValidPetPoses, getValidPoses, getClosestPose, } from "./components/SpeciesColorPicker"; -import useCurrentUser from "./components/useCurrentUser"; import SpeciesFacesPicker, { colorIsBasic, } 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. -// TODO: Refactor this stuff, do we even need ItemPageContent really? -// function ItemPage() { -// const { query } = useRouter(); -// return ; -// } - -/** - * 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 ; - } - - const item = data?.item; - - return ( - <> - - - - - - {isLoggedIn && } - - {!isEmbedded && } - - - - ); -} - -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 ( - - {description ? ( - description - ) : description === "" ? ( - (This item has no description.) - ) : ( - - - - - - )} - - ); -} - -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 {error.message}; - } - - 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 ( - - - - - = 1} - popoverPlacement="bottom-end" - /> - - - - - = 1} - popoverPlacement="bottom-start" - /> - - ); -} - -function ItemPageOwnWantListsDropdown({ - closetLists, - item, - isVisible, - popoverPlacement, -}) { - return ( - - - - - - - - - ); -} - -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 ( - - ); - }, -); - -function ItemPageOwnWantListsDropdownContent({ closetLists, item }) { - return ( - - {closetLists.map((closetList) => ( - - - - ))} - - ); -} - -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 ( - - {closetList.name} - - ); -} - -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 ( - - {({ css }) => ( - - { - 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, - }); - }); - } - }} - /> - - - )} - - ); -} - -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 ( - - {({ css }) => ( - - { - 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, - }); - }); - } - }} - /> - - - )} - - ); -} - -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 {error.message}; - } - - return ( - - - Trading: - - - - - - - - - ); -} - -function ItemPageTradeLink({ href, count, label, colorScheme, isEmbedded }) { - return ( - - ); -} - -function IconCheckbox({ icon, isChecked, ...props }) { - return ( - - - - {icon} - - - ); -} - -export function ItemPageOutfitPreview({ itemId }) { +function ItemPageOutfitPreview({ itemId }) { const idealPose = React.useMemo( () => (Math.random() > 0.5 ? "HAPPY_FEM" : "HAPPY_MASC"), [], @@ -1260,10 +526,7 @@ function PlayPauseButton({ isPaused, onClick }) { ); } -export function ItemZonesInfo({ - compatibleBodiesAndTheirZones, - restrictedZones, -}) { +function ItemZonesInfo({ compatibleBodiesAndTheirZones, restrictedZones }) { // 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 // generally works! @@ -1434,3 +697,5 @@ function buildSortKeyForZoneLabelsAndTheirBodies({ zoneLabel, bodies }) { return `${representsAllBodies ? "A" : "Z"}-${inverseBodyCount}-${zoneLabel}`; } + +export default ItemPageOutfitPreview; diff --git a/app/javascript/wardrobe-2020/WardrobePage/Item.js b/app/javascript/wardrobe-2020/WardrobePage/Item.js index 8086fb00..b341ad81 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/Item.js +++ b/app/javascript/wardrobe-2020/WardrobePage/Item.js @@ -24,7 +24,6 @@ import { import SupportOnly from "./support/SupportOnly"; import useSupport from "./support/useSupport"; -const LoadableItemPageDrawer = loadable(() => import("../ItemPageDrawer")); const LoadableItemSupportDrawer = loadable(() => import("./support/ItemSupportDrawer"), ); @@ -56,7 +55,6 @@ function Item({ onRemove, isDisabled = false, }) { - const [infoDrawerIsOpen, setInfoDrawerIsOpen] = React.useState(false); const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false); return ( @@ -97,24 +95,10 @@ function Item({ icon={} label="More info" to={`/items/${item.id}`} - onClick={(e) => { - const willProbablyOpenInNewTab = - e.metaKey || e.shiftKey || e.altKey || e.ctrlKey; - if (willProbablyOpenInNewTab) { - return; - } - - setInfoDrawerIsOpen(true); - e.preventDefault(); - }} + target="_blank" /> - setInfoDrawerIsOpen(false)} - /> (