impress-2020/src/app/SpeciesColorPicker.js

171 lines
4.8 KiB
JavaScript
Raw Normal View History

2020-04-25 04:33:05 -07:00
import React from "react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/react-hooks";
2020-05-10 00:21:04 -07:00
import { Box, Flex, Select, Text } from "@chakra-ui/core";
2020-04-25 04:33:05 -07:00
2020-05-17 23:44:33 -07:00
import { Delay, useFetch } from "./util";
2020-04-25 04:33:05 -07:00
2020-04-26 01:44:26 -07:00
/**
* SpeciesColorPicker lets the user pick the species/color of their pet.
*
* It preloads all species, colors, and valid species/color pairs; and then
* ensures that the outfit is always in a valid state.
*/
2020-05-10 00:21:04 -07:00
function SpeciesColorPicker({
speciesId,
colorId,
showPlaceholders,
dark = false,
onChange,
}) {
const { loading: loadingMeta, error: errorMeta, data: meta } = useQuery(gql`
2020-05-19 14:48:54 -07:00
query SpeciesColorPicker {
2020-04-25 04:33:05 -07:00
allSpecies {
id
name
}
allColors {
id
name
}
}
`);
const {
loading: loadingValids,
error: errorValids,
data: validsBuffer,
2020-05-17 23:44:33 -07:00
} = useFetch("/api/validPetPoses", { responseType: "arrayBuffer" });
const valids = React.useMemo(
() => validsBuffer && new DataView(validsBuffer),
[validsBuffer]
);
2020-04-25 04:33:05 -07:00
const allColors = (meta && [...meta.allColors]) || [];
2020-04-25 04:33:05 -07:00
allColors.sort((a, b) => a.name.localeCompare(b.name));
const allSpecies = (meta && [...meta.allSpecies]) || [];
2020-04-25 04:33:05 -07:00
allSpecies.sort((a, b) => a.name.localeCompare(b.name));
2020-04-26 01:44:26 -07:00
2020-05-10 00:21:04 -07:00
const backgroundColor = dark ? "gray.600" : "white";
const borderColor = dark ? "transparent" : "green.600";
const textColor = dark ? "gray.50" : "inherit";
const SpeciesColorSelect = ({ ...props }) => (
<Select
backgroundColor={backgroundColor}
color={textColor}
border="1px"
borderColor={borderColor}
boxShadow="md"
width="auto"
_hover={{
borderColor: "green.400",
}}
_disabled={{
// Visually the disabled state is the same as the normal state, but
// with a wait cursor. We don't expect this to take long, and the flash
// of content is rough! (The caret still flashes, but that's small and
// harder to style in Chakra.)
opacity: 1,
cursor: "wait",
}}
isInvalid={valids && !pairIsValid(valids, speciesId, colorId)}
errorBorderColor="red.300"
2020-05-10 00:21:04 -07:00
{...props}
/>
);
if ((loadingMeta || loadingValids) && !showPlaceholders) {
2020-04-25 04:33:05 -07:00
return (
<Delay ms={5000}>
2020-05-10 00:21:04 -07:00
<Text color={textColor} textShadow="md">
2020-04-25 04:33:05 -07:00
Loading species/color data
</Text>
</Delay>
);
}
if (errorMeta || errorValids) {
2020-04-25 04:33:05 -07:00
return (
2020-05-10 00:21:04 -07:00
<Text color={textColor} textShadow="md">
2020-04-25 04:33:05 -07:00
Error loading species/color data.
</Text>
);
}
2020-04-26 01:44:26 -07:00
// When the color changes, check if the new pair is valid, and update the
// outfit if so!
2020-04-25 04:33:05 -07:00
const onChangeColor = (e) => {
2020-05-10 00:21:04 -07:00
const newColorId = e.target.value;
const species = allSpecies.find((s) => s.id === speciesId);
const newColor = allColors.find((c) => c.id === newColorId);
onChange(species, newColor, pairIsValid(valids, speciesId, newColorId));
2020-04-25 04:33:05 -07:00
};
2020-04-26 01:44:26 -07:00
// When the species changes, check if the new pair is valid, and update the
// outfit if so!
2020-04-25 04:33:05 -07:00
const onChangeSpecies = (e) => {
2020-05-10 00:21:04 -07:00
const newSpeciesId = e.target.value;
const newSpecies = allSpecies.find((s) => s.id === newSpeciesId);
const color = allColors.find((c) => c.id === colorId);
onChange(newSpecies, color, pairIsValid(valids, newSpeciesId, colorId));
2020-04-25 04:33:05 -07:00
};
return (
<Flex direction="row">
2020-05-10 00:21:04 -07:00
<SpeciesColorSelect
2020-04-25 04:33:05 -07:00
aria-label="Pet color"
2020-05-10 00:21:04 -07:00
value={colorId}
isDisabled={allColors.length === 0}
2020-04-25 04:33:05 -07:00
onChange={onChangeColor}
>
2020-05-10 00:21:04 -07:00
{allColors.length === 0 && (
<>
{/* The default case, and a long name for sizing! */}
<option>Blue</option>
<option>Dimensional</option>
</>
)}
2020-04-25 04:33:05 -07:00
{allColors.map((color) => (
<option key={color.id} value={color.id}>
{color.name}
</option>
))}
2020-05-10 00:21:04 -07:00
</SpeciesColorSelect>
2020-04-28 01:14:07 -07:00
<Box width="4" />
2020-05-10 00:21:04 -07:00
<SpeciesColorSelect
2020-04-25 04:33:05 -07:00
aria-label="Pet species"
2020-05-10 00:21:04 -07:00
value={speciesId}
isDisabled={allSpecies.length === 0}
2020-04-25 04:33:05 -07:00
onChange={onChangeSpecies}
>
2020-05-10 00:21:04 -07:00
{allSpecies.length === 0 && (
<>
{/* The default case, and a long name for sizing! */}
<option>Acara</option>
<option>Tuskaninny</option>
</>
)}
2020-04-25 04:33:05 -07:00
{allSpecies.map((species) => (
<option key={species.id} value={species.id}>
{species.name}
</option>
))}
2020-05-10 00:21:04 -07:00
</SpeciesColorSelect>
2020-04-25 04:33:05 -07:00
</Flex>
);
}
function pairIsValid(valids, speciesId, colorId) {
// Reading a bit table, owo!
const speciesIndex = speciesId - 1;
const colorIndex = colorId - 1;
const numColors = valids.getUint8(1);
const pairByteIndex = speciesIndex * numColors + colorIndex + 2;
const pairByte = valids.getUint8(pairByteIndex);
return pairByte !== 0;
}
2020-04-25 04:33:05 -07:00
export default SpeciesColorPicker;