Show saved outfits in wardrobe page!

Still a pretty limited early version, no saving _back_ to the server. But you can click from the Your Outfits page and see the outfit for real! :3 We have a WIPCallout explaining the basics.
This commit is contained in:
Emi Matchu 2021-01-05 06:29:39 +00:00
parent 6749d19f9e
commit 1f5a9d60a2
5 changed files with 136 additions and 50 deletions

View file

@ -89,6 +89,9 @@ function App() {
<Route path="/outfits/new">
<WardrobePage />
</Route>
<Route path="/outfits/:id">
<WardrobePage />
</Route>
<Route path="/user/:userId/items">
<PageLayout>
<UserItemsPage />

View file

@ -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)`,

View file

@ -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,7 +242,8 @@ function ItemZoneGroupSkeleton({ itemCount }) {
*/
function OutfitHeading({ outfitState, dispatchToOutfit }) {
return (
<Box>
<Flex align="flex-start" justify="space-between">
<Box marginRight="4">
<Box role="group" d="inline-block" position="relative" width="100%">
<Heading1 mb="6">
<Editable
@ -276,6 +278,16 @@ function OutfitHeading({ outfitState, dispatchToOutfit }) {
</Heading1>
</Box>
</Box>
{outfitState.id && (
<WIPCallout
details={`To save a new version of this outfit, use Classic DTI. But you can still play around in here for now!`}
marginTop="1"
placement="bottom-end"
>
Saved outfits are WIP!
</WIPCallout>
)}
</Flex>
);
}

View file

@ -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;

View file

@ -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}
>
<Box
as="img"
@ -47,10 +48,10 @@ function WIPCallout({ children, details }) {
content = (
<Tooltip
label={<Box textAlign="center">{details}</Box>}
placement="bottom"
placement={placement}
shouldWrapChildren
>
{content}
<Box cursor="help">{content}</Box>
</Tooltip>
);
}