link sharing ^^

This commit is contained in:
Matt Dunn-Rankin 2020-04-25 07:22:03 -07:00
parent 90ad9feb0c
commit 26244ea776
3 changed files with 135 additions and 47 deletions

View file

@ -10,8 +10,10 @@ import {
Image, Image,
PseudoBox, PseudoBox,
Spinner, Spinner,
Stack,
Text, Text,
Tooltip, Tooltip,
useClipboard,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { Delay } from "./util"; import { Delay } from "./util";
@ -68,6 +70,8 @@ function OutfitPreview({ outfitState, dispatchToOutfit }) {
visibleLayers visibleLayers
); );
const { onCopy, hasCopied } = useClipboard(getShareUrl(outfitState));
if (error) { if (error) {
return ( return (
<FullScreenCenter> <FullScreenCenter>
@ -133,9 +137,9 @@ function OutfitPreview({ outfitState, dispatchToOutfit }) {
bottom={{ base: 2, lg: 6 }} bottom={{ base: 2, lg: 6 }}
// Grid layout for the content! // Grid layout for the content!
display="grid" display="grid"
gridTemplateAreas={`"space picker download"`} gridTemplateAreas={`"space picker buttons"`}
gridTemplateColumns="minmax(0, 1fr) auto 1fr" gridTemplateColumns="minmax(0, 1fr) auto 1fr"
alignItems="center" alignItems="flex-end"
> >
<Box gridArea="space"></Box> <Box gridArea="space"></Box>
<PseudoBox <PseudoBox
@ -151,46 +155,83 @@ function OutfitPreview({ outfitState, dispatchToOutfit }) {
onBlur={() => setHasFocus(false)} onBlur={() => setHasFocus(false)}
/> />
</PseudoBox> </PseudoBox>
<Flex gridArea="download" justify="flex-end"> <Stack gridArea="buttons" spacing="2" align="flex-end">
<Tooltip label="Download" placement="left"> <Box>
<IconButton <Tooltip
icon="download" label={hasCopied ? "Copied!" : "Copy link"}
aria-label="Download" placement="left"
isRound >
as="a" <IconButton
// eslint-disable-next-line no-script-url icon={hasCopied ? "check" : "link"}
href={downloadImageUrl || "javascript:void 0"} aria-label="Copy link"
download={(outfitState.name || "Outfit") + ".png"} isRound
onMouseEnter={prepareDownload} onClick={onCopy}
onFocus={() => { onFocus={() => setHasFocus(true)}
prepareDownload(); onBlur={() => setHasFocus(false)}
setHasFocus(true); variant="unstyled"
}} backgroundColor="gray.600"
onBlur={() => setHasFocus(false)} color="gray.50"
cursor={!downloadImageUrl && "wait"} boxShadow="md"
variant="unstyled" d="flex"
backgroundColor="gray.600" alignItems="center"
color="gray.50" justifyContent="center"
boxShadow="md" opacity={hasFocus ? 1 : 0}
d="flex" transition="all 0.2s"
alignItems="center" _groupHover={{
justifyContent="center" opacity: 1,
opacity={hasFocus ? 1 : 0} }}
transition="all 0.2s" _focus={{
_groupHover={{ opacity: 1,
opacity: 1, backgroundColor: "gray.500",
}} }}
_focus={{ _hover={{
opacity: 1, backgroundColor: "gray.500",
backgroundColor: "gray.500", }}
}} outline="initial"
_hover={{ />
backgroundColor: "gray.500", </Tooltip>
}} </Box>
outline="initial" <Box>
/> <Tooltip label="Download" placement="left">
</Tooltip> <IconButton
</Flex> icon="download"
aria-label="Download"
isRound
as="a"
// eslint-disable-next-line no-script-url
href={downloadImageUrl || "javascript:void 0"}
download={(outfitState.name || "Outfit") + ".png"}
onMouseEnter={prepareDownload}
onFocus={() => {
prepareDownload();
setHasFocus(true);
}}
onBlur={() => setHasFocus(false)}
cursor={!downloadImageUrl && "wait"}
variant="unstyled"
backgroundColor="gray.600"
color="gray.50"
boxShadow="md"
d="flex"
alignItems="center"
justifyContent="center"
opacity={hasFocus ? 1 : 0}
transition="all 0.2s"
_groupHover={{
opacity: 1,
}}
_focus={{
opacity: 1,
backgroundColor: "gray.500",
}}
_hover={{
backgroundColor: "gray.500",
}}
outline="initial"
/>
</Tooltip>
</Box>
</Stack>
</Box> </Box>
<Box pos="absolute" left="3" top="3"> <Box pos="absolute" left="3" top="3">
<IconButton <IconButton
@ -315,4 +356,29 @@ function useDownloadableImage(visibleLayers) {
return [downloadImageUrl, prepareDownload]; return [downloadImageUrl, prepareDownload];
} }
function getShareUrl(outfitState) {
const {
name,
speciesId,
colorId,
wornItemIds,
closetedItemIds,
} = outfitState;
const params = new URLSearchParams();
params.append("name", name);
params.append("species", speciesId);
params.append("color", colorId);
for (const itemId of wornItemIds) {
params.append("objects[]", itemId);
}
for (const itemId of closetedItemIds) {
params.append("closet[]", itemId);
}
const { origin, pathname } = window.location;
const url = origin + pathname + "?" + params.toString();
return url;
}
export default OutfitPreview; export default OutfitPreview;

View file

@ -95,7 +95,9 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
offset: items.length, offset: items.length,
}, },
updateQuery: (prev, { fetchMoreResult }) => { updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev; if (!fetchMoreResult || fetchMoreResult.query !== prev.query) {
return prev;
}
// Note: This is a bit awkward because, if the results count ends on // Note: This is a bit awkward because, if the results count ends on
// a multiple of 30, the user will see a flash of loading before // a multiple of 30, the user will see a flash of loading before

View file

@ -29,6 +29,21 @@ function useOutfitState() {
} }
); );
React.useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("species")) {
dispatchToOutfit({
type: "reset",
name: urlParams.get("name"),
speciesId: urlParams.get("species"),
colorId: urlParams.get("color"),
wornItemIds: urlParams.getAll("objects[]"),
closetedItemIds: urlParams.getAll("closet[]"),
});
}
window.history.replaceState(null, "", window.location.href.split("?")[0]);
});
const { name, speciesId, colorId } = state; const { name, speciesId, colorId } = state;
// It's more convenient to manage these as a Set in state, but most callers // It's more convenient to manage these as a Set in state, but most callers
@ -82,6 +97,7 @@ function useOutfitState() {
zonesAndItems, zonesAndItems,
name, name,
wornItemIds, wornItemIds,
closetedItemIds,
allItemIds, allItemIds,
speciesId, speciesId,
colorId, colorId,
@ -150,10 +166,14 @@ const outfitStateReducer = (apolloClient) => (baseState, action) => {
const { name, speciesId, colorId, wornItemIds, closetedItemIds } = action; const { name, speciesId, colorId, wornItemIds, closetedItemIds } = action;
return { return {
name, name,
speciesId: String(speciesId || baseState.speciesId), speciesId: speciesId ? String(speciesId) : baseState.speciesId,
colorId: String(colorId || baseState.colorId), colorId: colorId ? String(colorId) : baseState.colorId,
wornItemIds: new Set(wornItemIds.map(String)), wornItemIds: wornItemIds
closetedItemIds: new Set(closetedItemIds.map(String)), ? new Set(wornItemIds.map(String))
: baseState.wornItemIds,
closetedItemIds: closetedItemIds
? new Set(closetedItemIds.map(String))
: baseState.closetedItemIds,
}; };
default: default:
throw new Error(`unexpected action ${JSON.stringify(action)}`); throw new Error(`unexpected action ${JSON.stringify(action)}`);