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:
parent
6749d19f9e
commit
1f5a9d60a2
5 changed files with 136 additions and 50 deletions
|
@ -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 />
|
||||
|
|
|
@ -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)`,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue