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;