diff --git a/src/app/HomePage.js b/src/app/HomePage.js index 3e89fa3..eb41a1f 100644 --- a/src/app/HomePage.js +++ b/src/app/HomePage.js @@ -2,7 +2,7 @@ import React from "react"; import { css } from "emotion"; import gql from "graphql-tag"; import { Box, Button, Flex, Input, useTheme, useToast } from "@chakra-ui/core"; -import { useHistory } from "react-router-dom"; +import { useHistory, useLocation } from "react-router-dom"; import { useLazyQuery } from "@apollo/client"; import { Heading1, usePageTitle } from "./util"; @@ -14,6 +14,7 @@ import SpeciesColorPicker from "./components/SpeciesColorPicker"; function HomePage() { usePageTitle("Dress to Impress"); + useSupportSetup(); const [previewState, setPreviewState] = React.useState(null); @@ -226,4 +227,48 @@ function SubmitPetForm() { ); } +/** + * useSupportSetup helps our support staff get set up with special access. + * If you provide ?supportSecret=... in the URL, we'll save it in a cookie and + * pop up a toast! + * + * This doesn't guarantee the secret is correct, of course! We don't bother to + * check that here; the server will reject requests from bad support secrets. + * And there's nothing especially secret in the support UI, so it's okay if + * other people know about the tools and poke around a powerless interface! + */ +function useSupportSetup() { + const location = useLocation(); + const toast = useToast(); + + React.useEffect(() => { + const params = new URLSearchParams(location.search); + const supportSecret = params.get("supportSecret"); + + if (supportSecret) { + localStorage.setItem("supportSecret", supportSecret); + + toast({ + title: "Support secret saved!", + description: + `You should now see special Support UI across the site. ` + + `Thanks for your help! 💖`, + status: "success", + duration: 10000, + isClosable: true, + }); + } else if (supportSecret === "") { + localStorage.removeItem("supportSecret"); + + toast({ + title: "Support secret deleted.", + description: `The Support UI will now stop appearing on this device.`, + status: "success", + duration: 10000, + isClosable: true, + }); + } + }, [location, toast]); +} + export default HomePage; diff --git a/src/app/WardrobePage/Item.js b/src/app/WardrobePage/Item.js index 52e46c3..af01d72 100644 --- a/src/app/WardrobePage/Item.js +++ b/src/app/WardrobePage/Item.js @@ -9,9 +9,15 @@ import { Tooltip, useTheme, } from "@chakra-ui/core"; -import { DeleteIcon, InfoIcon } from "@chakra-ui/icons"; +import { EditIcon, DeleteIcon, InfoIcon } from "@chakra-ui/icons"; +import loadable from "@loadable/component"; import { safeImageUrl } from "../util"; +import SupportOnly from "./support/SupportOnly"; + +const LoadableItemSupportDrawer = loadable(() => + import("./support/ItemSupportDrawer") +); /** * Item show a basic summary of an item, in the context of the current outfit! @@ -29,37 +35,58 @@ export function Item({ item, itemNameId, outfitState, dispatchToOutfit }) { const isWorn = wornItemIds.includes(item.id); const isInOutfit = allItemIds.includes(item.id); + const [supportDrawerIsOpen, setSupportDrawerIsOpen] = React.useState(false); + return ( - - - - - - - - {item.name} - - - - - } - label="More info" - href={`http://impress.openneo.net/items/${ - item.id - }-${item.name.replace(/ /g, "-")}`} - /> - {isInOutfit && ( - } - label="Remove" - onClick={() => - dispatchToOutfit({ type: "removeItem", itemId: item.id }) - } + <> + + + - )} - - + + + + + {item.name} + + + + + + } + label="Edit" + onClick={() => setSupportDrawerIsOpen(true)} + /> + + } + label="More info" + href={`https://impress.openneo.net/items/${ + item.id + }-${item.name.replace(/ /g, "-")}`} + /> + {isInOutfit && ( + } + label="Remove" + onClick={() => + dispatchToOutfit({ type: "removeItem", itemId: item.id }) + } + /> + )} + + + + setSupportDrawerIsOpen(false)} + /> + + ); } diff --git a/src/app/WardrobePage/support/ItemSupportDrawer.js b/src/app/WardrobePage/support/ItemSupportDrawer.js new file mode 100644 index 0000000..bec4ccc --- /dev/null +++ b/src/app/WardrobePage/support/ItemSupportDrawer.js @@ -0,0 +1,80 @@ +import * as React from "react"; +import { + Badge, + Box, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + FormControl, + FormHelperText, + FormLabel, + Link, + Select, +} from "@chakra-ui/core"; +import { ExternalLinkIcon } from "@chakra-ui/icons"; + +/** + * ItemSupportDrawer shows Support UI for the item when open. + * + * This component controls the drawer element. The actual content is imported + * from another lazy-loaded component! + */ +function ItemSupportDrawer({ item, isOpen, onClose }) { + return ( + + + + + + {item.name} + + Support + + + + + + + + + + + ); +} + +function SpecialColorFields({ item }) { + return ( + + Special color + + + This controls which previews we show on the{" "} + + item page + + . + + + ); +} + +export default ItemSupportDrawer; diff --git a/src/app/WardrobePage/support/ItemSupportDrawerContent.js b/src/app/WardrobePage/support/ItemSupportDrawerContent.js new file mode 100644 index 0000000..e69de29 diff --git a/src/app/WardrobePage/support/SupportOnly.js b/src/app/WardrobePage/support/SupportOnly.js new file mode 100644 index 0000000..33a71a5 --- /dev/null +++ b/src/app/WardrobePage/support/SupportOnly.js @@ -0,0 +1,24 @@ +import * as React from "react"; + +/** + * SupportOnly only shows its contents to Support users. For most users, the + * content will be hidden! + * + * To become a Support user, you visit /?supportSecret=..., which saves the + * secret to your device. + * + * Note that this component doesn't check that the secret is *correct*, so it's + * possible to view this UI by faking an invalid secret. That's okay, because + * the server checks the provided secret for each Support request. + */ +function SupportOnly({ children }) { + const supportSecret = React.useMemo(getSupportSecret, []); + + return supportSecret ? children : null; +} + +function getSupportSecret() { + return localStorage.getItem("supportSecret"); +} + +export default SupportOnly;