impress-2020/src/app/WardrobePage/support/PosePickerSupport.js
Matchu 34b69a5e2b Oops, show the 600x600 image in pet layer support
Previously, the PNG link for a pet layer would show the 150x150 version. This was both an inconvenient size, but also not reflective of how the layer actually behaved, because we only use Neopets's official PNG for the 600x600 version!
2021-04-21 22:08:15 -07:00

539 lines
14 KiB
JavaScript

import React from "react";
import gql from "graphql-tag";
import { useMutation, useQuery } from "@apollo/client";
import {
Box,
Button,
IconButton,
Select,
Spinner,
Switch,
Wrap,
WrapItem,
useDisclosure,
} from "@chakra-ui/react";
import {
ArrowBackIcon,
ArrowForwardIcon,
CheckCircleIcon,
EditIcon,
} from "@chakra-ui/icons";
import HangerSpinner from "../../components/HangerSpinner";
import Metadata, { MetadataLabel, MetadataValue } from "./Metadata";
import useSupport from "./useSupport";
import AppearanceLayerSupportModal from "./AppearanceLayerSupportModal";
function PosePickerSupport({
speciesId,
colorId,
pose,
appearanceId,
initialFocusRef,
dispatchToOutfit,
}) {
const { loading, error, data } = useQuery(
gql`
query PosePickerSupport($speciesId: ID!, $colorId: ID!) {
petAppearances(speciesId: $speciesId, colorId: $colorId) {
id
pose
isGlitched
layers {
id
zone {
id
label @client
}
# For AppearanceLayerSupportModal
remoteId
bodyId
swfUrl
svgUrl
imageUrl(size: SIZE_600)
canvasMovieLibraryUrl
}
# For AppearanceLayerSupportModal to know the name
species {
id
name
}
color {
id
name
}
}
...CanonicalPetAppearances
}
${canonicalPetAppearancesFragment}
`,
{ variables: { speciesId, colorId } }
);
// Resize the Popover when we toggle loading state, because it probably will
// affect the content size. appearanceId might also affect content size, if
// it occupies different zones.
//
// NOTE: This also triggers an additional necessary resize when the component
// first mounts, because PosePicker lazy-loads it, so it actually
// mounting affects size too.
React.useLayoutEffect(() => {
// HACK: To trigger a Popover resize, we simulate a window resize event,
// because Popover listens for window resizes to reposition itself.
// I've also filed an issue requesting an official API!
// https://github.com/chakra-ui/chakra-ui/issues/1853
window.dispatchEvent(new Event("resize"));
}, [loading, appearanceId]);
if (loading) {
return (
<Box display="flex" justifyContent="center">
<HangerSpinner size="sm" />
</Box>
);
}
if (error) {
return (
<Box color="red.400" marginTop="8">
{error.message}
</Box>
);
}
const canonicalAppearanceIdsByPose = {
HAPPY_MASC: data.happyMasc?.id,
SAD_MASC: data.sadMasc?.id,
SICK_MASC: data.sickMasc?.id,
HAPPY_FEM: data.happyFem?.id,
SAD_FEM: data.sadFem?.id,
SICK_FEM: data.sickFem?.id,
UNCONVERTED: data.unconverted?.id,
UNKNOWN: data.unknown?.id,
};
const canonicalAppearanceIds = Object.values(
canonicalAppearanceIdsByPose
).filter((id) => id);
if (!appearanceId) {
appearanceId = canonicalAppearanceIdsByPose[pose];
}
const currentPetAppearance = data.petAppearances.find(
(pa) => pa.id === appearanceId
);
if (!currentPetAppearance) {
return (
<Box color="red.400" marginTop="8">
Pet appearance with ID {JSON.stringify(appearanceId)} not found
</Box>
);
}
return (
<Box>
<PosePickerSupportNavigator
petAppearances={data.petAppearances}
currentPetAppearance={currentPetAppearance}
canonicalAppearanceIds={canonicalAppearanceIds}
dropdownRef={initialFocusRef}
dispatchToOutfit={dispatchToOutfit}
/>
<Metadata
fontSize="sm"
// Build a new copy of this tree when the appearance changes, to reset
// things like element focus and mutation state!
key={currentPetAppearance.id}
>
<MetadataLabel>DTI ID:</MetadataLabel>
<MetadataValue>{appearanceId}</MetadataValue>
<MetadataLabel>Pose:</MetadataLabel>
<MetadataValue>
<PosePickerSupportPoseFields
petAppearance={currentPetAppearance}
speciesId={speciesId}
colorId={colorId}
/>
</MetadataValue>
<MetadataLabel>Layers:</MetadataLabel>
<MetadataValue>
<Wrap spacing="1">
{currentPetAppearance.layers
.map((layer) => [`${layer.zone.label} (${layer.zone.id})`, layer])
.sort((a, b) => a[0].localeCompare(b[0]))
.map(([text, layer]) => (
<WrapItem>
<PetLayerSupportLink
outfitState={{ speciesId, colorId, pose }}
petAppearance={currentPetAppearance}
layer={layer}
>
{text}
<EditIcon marginLeft="1" />
</PetLayerSupportLink>
</WrapItem>
))}
</Wrap>
</MetadataValue>
</Metadata>
</Box>
);
}
function PetLayerSupportLink({ outfitState, petAppearance, layer, children }) {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button size="xs" onClick={onOpen}>
{children}
</Button>
<AppearanceLayerSupportModal
outfitState={outfitState}
petAppearance={petAppearance}
layer={layer}
isOpen={isOpen}
onClose={onClose}
/>
</>
);
}
function PosePickerSupportNavigator({
petAppearances,
currentPetAppearance,
canonicalAppearanceIds,
dropdownRef,
dispatchToOutfit,
}) {
const currentIndex = petAppearances.indexOf(currentPetAppearance);
const prevPetAppearance = petAppearances[currentIndex - 1];
const nextPetAppearance = petAppearances[currentIndex + 1];
return (
<Box
display="flex"
justifyContent="flex-end"
marginBottom="4"
// Space for the position-absolute PosePicker mode switcher
paddingLeft="12"
>
<IconButton
aria-label="Go to previous appearance"
icon={<ArrowBackIcon />}
size="sm"
marginRight="2"
isDisabled={prevPetAppearance == null}
onClick={() =>
dispatchToOutfit({
type: "setPose",
pose: prevPetAppearance.pose,
appearanceId: prevPetAppearance.id,
})
}
/>
<Select
size="sm"
width="auto"
value={currentPetAppearance.id}
ref={dropdownRef}
onChange={(e) => {
const id = e.target.value;
const petAppearance = petAppearances.find((pa) => pa.id === id);
dispatchToOutfit({
type: "setPose",
pose: petAppearance.pose,
appearanceId: petAppearance.id,
});
}}
>
{petAppearances.map((pa) => (
<option key={pa.id} value={pa.id}>
{POSE_NAMES[pa.pose]}{" "}
{canonicalAppearanceIds.includes(pa.id) && "⭐️"}
{pa.isGlitched && "👾"} ({pa.id})
</option>
))}
</Select>
<IconButton
aria-label="Go to next appearance"
icon={<ArrowForwardIcon />}
size="sm"
marginLeft="2"
isDisabled={nextPetAppearance == null}
onClick={() =>
dispatchToOutfit({
type: "setPose",
pose: nextPetAppearance.pose,
appearanceId: nextPetAppearance.id,
})
}
/>
</Box>
);
}
function PosePickerSupportPoseFields({ petAppearance, speciesId, colorId }) {
const { supportSecret } = useSupport();
const [mutatePose, poseMutation] = useMutation(
gql`
mutation PosePickerSupportSetPetAppearancePose(
$appearanceId: ID!
$pose: Pose!
$supportSecret: String!
) {
setPetAppearancePose(
appearanceId: $appearanceId
pose: $pose
supportSecret: $supportSecret
) {
id
pose
}
}
`,
{
refetchQueries: [
{
query: gql`
query PosePickerSupportRefetchCanonicalAppearances(
$speciesId: ID!
$colorId: ID!
) {
...CanonicalPetAppearances
}
${canonicalPetAppearancesFragment}
`,
variables: { speciesId, colorId },
},
],
}
);
const [mutateIsGlitched, isGlitchedMutation] = useMutation(
gql`
mutation PosePickerSupportSetPetAppearanceIsGlitched(
$appearanceId: ID!
$isGlitched: Boolean!
$supportSecret: String!
) {
setPetAppearanceIsGlitched(
appearanceId: $appearanceId
isGlitched: $isGlitched
supportSecret: $supportSecret
) {
id
isGlitched
}
}
`,
{
refetchQueries: [
{
query: gql`
query PosePickerSupportRefetchCanonicalAppearances(
$speciesId: ID!
$colorId: ID!
) {
...CanonicalPetAppearances
}
${canonicalPetAppearancesFragment}
`,
variables: { speciesId, colorId },
},
],
}
);
return (
<Box>
<Box display="flex" flexDirection="row" alignItems="center">
<Select
size="sm"
value={petAppearance.pose}
flex="0 1 200px"
icon={
poseMutation.loading ? (
<Spinner />
) : poseMutation.data ? (
<CheckCircleIcon />
) : undefined
}
onChange={(e) => {
const pose = e.target.value;
mutatePose({
variables: {
appearanceId: petAppearance.id,
pose,
supportSecret,
},
optimisticResponse: {
__typename: "Mutation",
setPetAppearancePose: {
__typename: "PetAppearance",
id: petAppearance.id,
pose,
},
},
}).catch((e) => {
/* Discard errors here; we'll show them in the UI! */
});
}}
isInvalid={poseMutation.error != null}
>
{Object.entries(POSE_NAMES).map(([pose, name]) => (
<option key={pose} value={pose}>
{name}
</option>
))}
</Select>
<Select
size="sm"
marginLeft="2"
flex="0 1 150px"
value={petAppearance.isGlitched}
icon={
isGlitchedMutation.loading ? (
<Spinner />
) : isGlitchedMutation.data ? (
<CheckCircleIcon />
) : undefined
}
onChange={(e) => {
const isGlitched = e.target.value === "true";
mutateIsGlitched({
variables: {
appearanceId: petAppearance.id,
isGlitched,
supportSecret,
},
optimisticResponse: {
__typename: "Mutation",
setPetAppearanceIsGlitched: {
__typename: "PetAppearance",
id: petAppearance.id,
isGlitched,
},
},
}).catch((e) => {
/* Discard errors here; we'll show them in the UI! */
});
}}
isInvalid={isGlitchedMutation.error != null}
>
<option value="false">Valid</option>
<option value="true">Glitched</option>
</Select>
</Box>
{poseMutation.error && (
<Box color="red.400">{poseMutation.error.message}</Box>
)}
{isGlitchedMutation.error && (
<Box color="red.400">{isGlitchedMutation.error.message}</Box>
)}
</Box>
);
}
export function PosePickerSupportSwitch({ isChecked, onChange }) {
return (
<Box as="label" display="flex" flexDirection="row" alignItems="center">
<Box fontSize="sm">
<span role="img" aria-label="Support">
💖
</span>
</Box>
<Switch
colorScheme="pink"
marginLeft="1"
size="sm"
isChecked={isChecked}
onChange={onChange}
/>
</Box>
);
}
const POSE_NAMES = {
HAPPY_MASC: "Happy Masc",
HAPPY_FEM: "Happy Fem",
SAD_MASC: "Sad Masc",
SAD_FEM: "Sad Fem",
SICK_MASC: "Sick Masc",
SICK_FEM: "Sick Fem",
UNCONVERTED: "Unconverted",
UNKNOWN: "Unknown",
};
const canonicalPetAppearancesFragment = gql`
fragment CanonicalPetAppearances on Query {
happyMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_MASC
) {
id
}
sadMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_MASC
) {
id
}
sickMasc: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_MASC
) {
id
}
happyFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: HAPPY_FEM
) {
id
}
sadFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SAD_FEM
) {
id
}
sickFem: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: SICK_FEM
) {
id
}
unconverted: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: UNCONVERTED
) {
id
}
unknown: petAppearance(
speciesId: $speciesId
colorId: $colorId
pose: UNKNOWN
) {
id
}
}
`;
export default PosePickerSupport;