diff --git a/src/app/App.js b/src/app/App.js
index cfa0ee2..3c1635a 100644
--- a/src/app/App.js
+++ b/src/app/App.js
@@ -89,6 +89,9 @@ function App() {
+
+
+
diff --git a/src/app/UserOutfitsPage.js b/src/app/UserOutfitsPage.js
index 5c645e0..d7c4a47 100644
--- a/src/app/UserOutfitsPage.js
+++ b/src/app/UserOutfitsPage.js
@@ -2,6 +2,7 @@ import React from "react";
import { Box, Center, Flex, Wrap, WrapItem } from "@chakra-ui/react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
+import { Link } from "react-router-dom";
import { ErrorMessage, Heading1, useCommonStyles } from "./util";
import {
@@ -112,8 +113,8 @@ function OutfitCard({ outfit }) {
width="calc(150px + 2em)"
backgroundColor={brightBackground}
transition="all 0.2s"
- as="a"
- href={`https://impress.openneo.net/outfits/${outfit.id}`}
+ as={Link}
+ to={`/outfits/${outfit.id}`}
_hover={{ transform: `scale(1.05)` }}
_focus={{
transform: `scale(1.05)`,
diff --git a/src/app/WardrobePage/ItemsPanel.js b/src/app/WardrobePage/ItemsPanel.js
index 705fc10..e010656 100644
--- a/src/app/WardrobePage/ItemsPanel.js
+++ b/src/app/WardrobePage/ItemsPanel.js
@@ -16,6 +16,7 @@ import { CSSTransition, TransitionGroup } from "react-transition-group";
import { Delay, Heading1, Heading2 } from "../util";
import Item, { ItemListContainer, ItemListSkeleton } from "./Item";
+import WIPCallout from "../components/WIPCallout";
/**
* ItemsPanel shows the items in the current outfit, and lets the user toggle
@@ -241,41 +242,52 @@ function ItemZoneGroupSkeleton({ itemCount }) {
*/
function OutfitHeading({ outfitState, dispatchToOutfit }) {
return (
-
-
-
-
- dispatchToOutfit({ type: "rename", outfitName: value })
- }
- >
- {({ isEditing, onEdit }) => (
-
-
-
- {!isEditing && (
-
- }
- variant="link"
- aria-label="Edit outfit name"
- title="Edit outfit name"
- />
-
- )}
-
- )}
-
-
+
+
+
+
+
+ dispatchToOutfit({ type: "rename", outfitName: value })
+ }
+ >
+ {({ isEditing, onEdit }) => (
+
+
+
+ {!isEditing && (
+
+ }
+ variant="link"
+ aria-label="Edit outfit name"
+ title="Edit outfit name"
+ />
+
+ )}
+
+ )}
+
+
+
-
+ {outfitState.id && (
+
+ Saved outfits are WIP!
+
+ )}
+
);
}
diff --git a/src/app/WardrobePage/useOutfitState.js b/src/app/WardrobePage/useOutfitState.js
index a10e457..1017b94 100644
--- a/src/app/WardrobePage/useOutfitState.js
+++ b/src/app/WardrobePage/useOutfitState.js
@@ -2,6 +2,7 @@ import React from "react";
import gql from "graphql-tag";
import produce, { enableMapSet } from "immer";
import { useQuery, useApolloClient } from "@apollo/client";
+import { useParams } from "react-router-dom";
import { itemAppearanceFragment } from "../components/useOutfitAppearance";
@@ -11,21 +12,72 @@ export const OutfitStateContext = React.createContext(null);
function useOutfitState() {
const apolloClient = useApolloClient();
- const initialState = parseOutfitUrl();
+ const initialState = useParseOutfitUrl();
const [state, dispatchToOutfit] = React.useReducer(
outfitStateReducer(apolloClient),
initialState
);
- const { name, speciesId, colorId, pose, appearanceId } = state;
+ const { id, name, speciesId, colorId, pose, appearanceId } = state;
// It's more convenient to manage these as a Set in state, but most callers
// will find it more convenient to access them as arrays! e.g. for `.map()`
const wornItemIds = Array.from(state.wornItemIds);
const closetedItemIds = Array.from(state.closetedItemIds);
-
const allItemIds = [...state.wornItemIds, ...state.closetedItemIds];
- const { loading, error, data } = useQuery(
+
+ // If there's an outfit ID (i.e. we're on /outfits/:id), load basic data
+ // about the outfit. We'll use it to initialize the local state.
+ const { loading: outfitLoading, error: outfitError } = useQuery(
+ gql`
+ query OutfitStateSavedOutfit($id: ID!) {
+ outfit(id: $id) {
+ id
+ name
+ petAppearance {
+ species {
+ id
+ }
+ color {
+ id
+ }
+ pose
+ }
+ wornItems {
+ id
+ }
+ closetedItems {
+ id
+ }
+
+ # TODO: Consider pre-loading some fields, instead of doing them in
+ # follow-up queries?
+ }
+ }
+ `,
+ {
+ variables: { id },
+ skip: id == null,
+ onCompleted: (outfitData) => {
+ const outfit = outfitData.outfit;
+ dispatchToOutfit({
+ type: "reset",
+ name: outfit.name,
+ speciesId: outfit.petAppearance.species.id,
+ colorId: outfit.petAppearance.color.id,
+ pose: outfit.petAppearance.pose,
+ wornItemIds: outfit.wornItems.map((item) => item.id),
+ closetedItemIds: outfit.closetedItems.map((item) => item.id),
+ });
+ },
+ }
+ );
+
+ const {
+ loading: itemsLoading,
+ error: itemsError,
+ data: itemsData,
+ } = useQuery(
gql`
query OutfitStateItems(
$allItemIds: [ID!]!
@@ -69,11 +121,14 @@ function useOutfitState() {
`,
{
variables: { allItemIds, speciesId, colorId },
- skip: allItemIds.length === 0,
+ // Skip if this outfit has no items, as an optimization; or if we don't
+ // have the species/color ID loaded yet because we're waiting on the
+ // saved outfit to load.
+ skip: allItemIds.length === 0 || speciesId == null || colorId == null,
}
);
- const resultItems = data?.items || [];
+ const resultItems = itemsData?.items || [];
// Okay, time for some big perf hacks! Lower down in the app, we use
// React.memo to avoid re-rendering Item components if the items haven't
@@ -123,6 +178,7 @@ function useOutfitState() {
const url = buildOutfitUrl(state);
const outfitState = {
+ id,
zonesAndItems,
incompatibleItems,
name,
@@ -141,7 +197,12 @@ function useOutfitState() {
window.history.replaceState(null, "", url);
}, [url]);
- return { loading, error, outfitState, dispatchToOutfit };
+ return {
+ loading: outfitLoading || itemsLoading,
+ error: outfitError || itemsError,
+ outfitState,
+ dispatchToOutfit,
+ };
}
const outfitStateReducer = (apolloClient) => (baseState, action) => {
@@ -249,9 +310,12 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
}
};
-function parseOutfitUrl() {
+function useParseOutfitUrl() {
+ const { id } = useParams();
const urlParams = new URLSearchParams(window.location.search);
+
return {
+ id: id,
name: urlParams.get("name"),
speciesId: urlParams.get("species"),
colorId: urlParams.get("color"),
@@ -440,6 +504,7 @@ function getZonesAndItems(itemsById, wornItemIds, closetedItemIds) {
function buildOutfitUrl(state) {
const {
+ id,
name,
speciesId,
colorId,
@@ -449,6 +514,12 @@ function buildOutfitUrl(state) {
closetedItemIds,
} = state;
+ const { origin, pathname } = window.location;
+
+ if (id) {
+ return origin + `/outfits/${id}`;
+ }
+
const params = new URLSearchParams({
name: name || "",
species: speciesId,
@@ -467,9 +538,7 @@ function buildOutfitUrl(state) {
params.append("state", appearanceId);
}
- const { origin, pathname } = window.location;
- const url = origin + pathname + "?" + params.toString();
- return url;
+ return origin + pathname + "?" + params.toString();
}
export default useOutfitState;
diff --git a/src/app/components/WIPCallout.js b/src/app/components/WIPCallout.js
index 6ac0065..bf2491f 100644
--- a/src/app/components/WIPCallout.js
+++ b/src/app/components/WIPCallout.js
@@ -6,7 +6,7 @@ import { useCommonStyles } from "../util";
import WIPXweeImg from "../images/wip-xwee.png";
import WIPXweeImg2x from "../images/wip-xwee@2x.png";
-function WIPCallout({ children, details }) {
+function WIPCallout({ children, details, placement = "bottom", ...props }) {
const { brightBackground } = useCommonStyles();
let content = (
@@ -22,6 +22,7 @@ function WIPCallout({ children, details }) {
paddingRight="4"
paddingY="1"
fontSize="sm"
+ {...props}
>
{details}}
- placement="bottom"
+ placement={placement}
shouldWrapChildren
>
- {content}
+ {content}
);
}