Show selected color faces for SpeciesFacesPicker
This commit is contained in:
parent
19482be2b8
commit
a42e696955
3 changed files with 157 additions and 7 deletions
|
@ -18,6 +18,7 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
|
Flex,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
@ -25,6 +26,7 @@ import {
|
||||||
EditIcon,
|
EditIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
|
WarningTwoIcon,
|
||||||
} from "@chakra-ui/icons";
|
} from "@chakra-ui/icons";
|
||||||
import { MdPause, MdPlayArrow } from "react-icons/md";
|
import { MdPause, MdPlayArrow } from "react-icons/md";
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
@ -717,6 +719,7 @@ function ItemPageOutfitPreview({ itemId }) {
|
||||||
<Box maxWidth="400px">
|
<Box maxWidth="400px">
|
||||||
<SpeciesFacesPicker
|
<SpeciesFacesPicker
|
||||||
selectedSpeciesId={petState.speciesId}
|
selectedSpeciesId={petState.speciesId}
|
||||||
|
selectedColorId={petState.colorId}
|
||||||
compatibleBodies={compatibleBodies}
|
compatibleBodies={compatibleBodies}
|
||||||
couldProbablyModelMoreData={couldProbablyModelMoreData}
|
couldProbablyModelMoreData={couldProbablyModelMoreData}
|
||||||
onChange={({ speciesId, colorId }) =>
|
onChange={({ speciesId, colorId }) =>
|
||||||
|
@ -785,22 +788,73 @@ function PlayPauseButton({ isPaused, onClick }) {
|
||||||
|
|
||||||
function SpeciesFacesPicker({
|
function SpeciesFacesPicker({
|
||||||
selectedSpeciesId,
|
selectedSpeciesId,
|
||||||
|
selectedColorId,
|
||||||
compatibleBodies,
|
compatibleBodies,
|
||||||
couldProbablyModelMoreData,
|
couldProbablyModelMoreData,
|
||||||
onChange,
|
onChange,
|
||||||
isLoading,
|
isLoading,
|
||||||
}) {
|
}) {
|
||||||
|
// For basic colors (Blue, Green, Red, Yellow), we just use the hardcoded
|
||||||
|
// data, which is part of the bundle and loads super-fast. For other colors,
|
||||||
|
// we load in all the faces of that color, falling back to basic colors when
|
||||||
|
// absent!
|
||||||
|
//
|
||||||
|
// TODO: Could we move this into our `build-cached-data` script, and just do
|
||||||
|
// the query all the time, and have Apollo happen to satisfy it fast?
|
||||||
|
// The semantics of returning our colorful random set could be weird…
|
||||||
|
const selectedColorIsBasic = ["8", "34", "61", "84"].includes(
|
||||||
|
selectedColorId
|
||||||
|
);
|
||||||
|
const { loading: loadingGQL, error, data } = useQuery(
|
||||||
|
gql`
|
||||||
|
query SpeciesFacesPicker($selectedColorId: ID!) {
|
||||||
|
color(id: $selectedColorId) {
|
||||||
|
id
|
||||||
|
appliedToAllCompatibleSpecies {
|
||||||
|
id
|
||||||
|
neopetsImageHash
|
||||||
|
species {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
variables: { selectedColorId },
|
||||||
|
skip: selectedColorId == null || selectedColorIsBasic,
|
||||||
|
onError: (e) => console.error(e),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const allBodiesAreCompatible = compatibleBodies.some(
|
const allBodiesAreCompatible = compatibleBodies.some(
|
||||||
(body) => body.representsAllBodies
|
(body) => body.representsAllBodies
|
||||||
);
|
);
|
||||||
const compatibleBodyIds = compatibleBodies.map((body) => body.id);
|
const compatibleBodyIds = compatibleBodies.map((body) => body.id);
|
||||||
|
|
||||||
const allSpeciesFaces = speciesFaces.sort((a, b) =>
|
const speciesFacesFromData = data?.color?.appliedToAllCompatibleSpecies || [];
|
||||||
a.speciesName.localeCompare(b.speciesName)
|
|
||||||
);
|
const allSpeciesFaces = DEFAULT_SPECIES_FACES.map((defaultSpeciesFace) => {
|
||||||
|
const providedSpeciesFace = speciesFacesFromData.find(
|
||||||
|
(f) => f.species.id === defaultSpeciesFace.speciesId
|
||||||
|
);
|
||||||
|
if (providedSpeciesFace && providedSpeciesFace.neopetsImageHash) {
|
||||||
|
return {
|
||||||
|
...defaultSpeciesFace,
|
||||||
|
colorId: selectedColorId,
|
||||||
|
bodyId: providedSpeciesFace.body.id,
|
||||||
|
neopetsImageHash: providedSpeciesFace.neopetsImageHash,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return defaultSpeciesFace;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box>
|
||||||
<Wrap
|
<Wrap
|
||||||
spacing="0"
|
spacing="0"
|
||||||
justify="center"
|
justify="center"
|
||||||
|
@ -824,11 +878,29 @@ function SpeciesFacesPicker({
|
||||||
isSelected={speciesFace.speciesId === selectedSpeciesId}
|
isSelected={speciesFace.speciesId === selectedSpeciesId}
|
||||||
couldProbablyModelMoreData={couldProbablyModelMoreData}
|
couldProbablyModelMoreData={couldProbablyModelMoreData}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading || loadingGQL}
|
||||||
/>
|
/>
|
||||||
</WrapItem>
|
</WrapItem>
|
||||||
))}
|
))}
|
||||||
</Wrap>
|
</Wrap>
|
||||||
|
{error && (
|
||||||
|
<Flex
|
||||||
|
color="yellow.500"
|
||||||
|
fontSize="xs"
|
||||||
|
marginTop="1"
|
||||||
|
textAlign="center"
|
||||||
|
width="100%"
|
||||||
|
align="flex-start"
|
||||||
|
justify="center"
|
||||||
|
>
|
||||||
|
<WarningTwoIcon marginTop="0.4em" marginRight="1" />
|
||||||
|
<Box>
|
||||||
|
Error loading this color's thumbnail images.
|
||||||
|
<br />
|
||||||
|
Check your connection and try again.
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
<Global
|
<Global
|
||||||
// Workaround for https://github.com/chakra-ui/chakra-ui/issues/3257,
|
// Workaround for https://github.com/chakra-ui/chakra-ui/issues/3257,
|
||||||
// which causes tooltip hover flicker.
|
// which causes tooltip hover flicker.
|
||||||
|
@ -838,7 +910,7 @@ function SpeciesFacesPicker({
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1062,7 +1134,7 @@ function DeferredTooltip({ children, isOpen, ...props }) {
|
||||||
// And it's not so bad if this gets out of sync with the database,
|
// And it's not so bad if this gets out of sync with the database,
|
||||||
// because the SpeciesColorPicker will still be usable!
|
// because the SpeciesColorPicker will still be usable!
|
||||||
const colors = { BLUE: "8", RED: "61", GREEN: "34", YELLOW: "84" };
|
const colors = { BLUE: "8", RED: "61", GREEN: "34", YELLOW: "84" };
|
||||||
const speciesFaces = [
|
const DEFAULT_SPECIES_FACES = [
|
||||||
{
|
{
|
||||||
speciesName: "Acara",
|
speciesName: "Acara",
|
||||||
speciesId: "1",
|
speciesId: "1",
|
||||||
|
|
|
@ -742,6 +742,29 @@ const buildPetTypeBySpeciesAndColorLoader = (db, loaders) =>
|
||||||
{ cacheKeyFn: ({ speciesId, colorId }) => `${speciesId},${colorId}` }
|
{ cacheKeyFn: ({ speciesId, colorId }) => `${speciesId},${colorId}` }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const buildPetTypesForColorLoader = (db, loaders) =>
|
||||||
|
new DataLoader(async (colorIds) => {
|
||||||
|
const qs = colorIds.map((_) => "?").join(",");
|
||||||
|
const [rows, _] = await db.execute(
|
||||||
|
`SELECT * FROM pet_types WHERE color_id IN (${qs})`,
|
||||||
|
colorIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const entities = rows.map(normalizeRow);
|
||||||
|
|
||||||
|
for (const petType of entities) {
|
||||||
|
loaders.petTypeLoader.prime(petType.id, petType);
|
||||||
|
loaders.petTypeBySpeciesAndColorLoader.prime(
|
||||||
|
{ speciesId: petType.speciesId, colorId: petType.colorId },
|
||||||
|
petType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorIds.map((colorId) =>
|
||||||
|
entities.filter((e) => e.colorId === colorId)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const buildSwfAssetLoader = (db) =>
|
const buildSwfAssetLoader = (db) =>
|
||||||
new DataLoader(async (swfAssetIds) => {
|
new DataLoader(async (swfAssetIds) => {
|
||||||
const qs = swfAssetIds.map((_) => "?").join(",");
|
const qs = swfAssetIds.map((_) => "?").join(",");
|
||||||
|
@ -1271,6 +1294,7 @@ function buildLoaders(db) {
|
||||||
db,
|
db,
|
||||||
loaders
|
loaders
|
||||||
);
|
);
|
||||||
|
loaders.petTypesForColorLoader = buildPetTypesForColorLoader(db, loaders);
|
||||||
loaders.swfAssetLoader = buildSwfAssetLoader(db);
|
loaders.swfAssetLoader = buildSwfAssetLoader(db);
|
||||||
loaders.swfAssetCountLoader = buildSwfAssetCountLoader(db);
|
loaders.swfAssetCountLoader = buildSwfAssetCountLoader(db);
|
||||||
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);
|
loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db);
|
||||||
|
|
|
@ -14,6 +14,9 @@ const typeDefs = gql`
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
isStandard: Boolean!
|
isStandard: Boolean!
|
||||||
|
|
||||||
|
# All SpeciesColorPairs of this color.
|
||||||
|
appliedToAllCompatibleSpecies: [SpeciesColorPair!]! @cacheControl(maxAge: 1, staleWhileRevalidate: ${oneDay})
|
||||||
}
|
}
|
||||||
|
|
||||||
type Species @cacheControl(maxAge: ${oneWeek}) {
|
type Species @cacheControl(maxAge: ${oneWeek}) {
|
||||||
|
@ -68,6 +71,22 @@ const typeDefs = gql`
|
||||||
isGlitched: Boolean!
|
isGlitched: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Like a PetAppearance, but with no pose specified. Species and color are
|
||||||
|
# enough info to specify a body; and the neopetsImageHash values we save
|
||||||
|
# don't have gender presentation specified, anyway, so they're available
|
||||||
|
# here.
|
||||||
|
type SpeciesColorPair {
|
||||||
|
id: ID!
|
||||||
|
species: Species!
|
||||||
|
color: Color!
|
||||||
|
body: Body!
|
||||||
|
|
||||||
|
# A hash to use in a pets.neopets.com image URL. Might be null if we don't
|
||||||
|
# have one for this pair, which is uncommon - but it's _somewhat_ common
|
||||||
|
# for them to have clothes, if we've never seen a plain version modeled.
|
||||||
|
neopetsImageHash: String
|
||||||
|
}
|
||||||
|
|
||||||
extend type Query {
|
extend type Query {
|
||||||
color(id: ID!): Color
|
color(id: ID!): Color
|
||||||
allColors: [Color!]! @cacheControl(maxAge: ${oneHour}, staleWhileRevalidate: ${oneWeek})
|
allColors: [Color!]! @cacheControl(maxAge: ${oneHour}, staleWhileRevalidate: ${oneWeek})
|
||||||
|
@ -98,6 +117,15 @@ const resolvers = {
|
||||||
const color = await colorLoader.load(id);
|
const color = await colorLoader.load(id);
|
||||||
return color.standard ? true : false;
|
return color.standard ? true : false;
|
||||||
},
|
},
|
||||||
|
appliedToAllCompatibleSpecies: async (
|
||||||
|
{ id },
|
||||||
|
_,
|
||||||
|
{ petTypesForColorLoader }
|
||||||
|
) => {
|
||||||
|
const petTypes = await petTypesForColorLoader.load(id);
|
||||||
|
const speciesColorPairs = petTypes.map((petType) => ({ id: petType.id }));
|
||||||
|
return speciesColorPairs;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Species: {
|
Species: {
|
||||||
|
@ -193,6 +221,32 @@ const resolvers = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
SpeciesColorPair: {
|
||||||
|
species: async ({ id }, _, { petTypeLoader }) => {
|
||||||
|
const petType = await petTypeLoader.load(id);
|
||||||
|
return { id: petType.speciesId };
|
||||||
|
},
|
||||||
|
color: async ({ id }, _, { petTypeLoader }) => {
|
||||||
|
const petType = await petTypeLoader.load(id);
|
||||||
|
return { id: petType.colorId };
|
||||||
|
},
|
||||||
|
body: async ({ id }, _, { petTypeLoader }) => {
|
||||||
|
const petType = await petTypeLoader.load(id);
|
||||||
|
return { id: petType.bodyId };
|
||||||
|
},
|
||||||
|
neopetsImageHash: async ({ id }, _, { petTypeLoader }) => {
|
||||||
|
const petType = await petTypeLoader.load(id);
|
||||||
|
|
||||||
|
// `basicImageHash` is guaranteed to be a plain no-clothes image, whereas
|
||||||
|
// `imageHash` prefers to be if possible, but might not be. (I forget the
|
||||||
|
// details on how this was implemented in Classic, so I'm not _sure_ on
|
||||||
|
// `imageHash` preferences during modeling… but I'm confident that
|
||||||
|
// `basicImageHash` is always better, and `imageHash` is better than
|
||||||
|
// nothing!)
|
||||||
|
return petType.basicImageHash || petType.imageHash;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
Query: {
|
Query: {
|
||||||
color: async (_, { id }, { colorLoader }) => {
|
color: async (_, { id }, { colorLoader }) => {
|
||||||
const color = await colorLoader.load(id);
|
const color = await colorLoader.load(id);
|
||||||
|
|
Loading…
Reference in a new issue