import React from "react"; import { css } from "@emotion/react"; import { AspectRatio, Box, Button, Center, Flex, FormControl, FormErrorMessage, FormLabel, Grid, Input, InputGroup, InputRightElement, Popover, PopoverArrow, PopoverBody, PopoverContent, PopoverTrigger, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs, Textarea, useBreakpointValue, useClipboard, useColorModeValue, VStack, } from "@chakra-ui/react"; import { Delay, ErrorMessage, Heading1, Heading2 } from "./util"; import HangerSpinner from "./components/HangerSpinner"; import { gql, useQuery } from "@apollo/client"; import { CheckIcon, WarningIcon } from "@chakra-ui/icons"; function OutfitUrlsPage() { return ( <> Changing our outfit URLs

Hi, friends! Sorry for the trouble ๐Ÿ˜“ In short, by switching to the new outfit URLs below, we'll decrease our hosting costs by $20/month! ๐Ÿ™

The history

When we started hosting outfit images back in 2012, we didn't know a lot about web infrastructure, and we weren't thinking a lot about permanent URLs ๐Ÿ˜… We uploaded images directly to{" "} Amazon S3, and gave you Amazon's URL for them, at amazonaws.com.

Since then, we've grown a lot, and our Amazon costs have increased a lot too! These days, it costs about $30/month to serve outfit images from S3โ€”and $20 of that is just to store our millions of outfit images, including the ones nobody visits ๐Ÿ˜…

So, we've moved our apps to a new, more cost-efficient way to share outfit images! But, until we delete the old images from Amazon S3 altogether, we're still paying $20/month just to support the old amazonaws.com URLs.

I looked hard for a way to redirect the old Amazon URLs to our new service, but it seems to not be possible, and it seems like $20/month could be better spent another way ๐Ÿ˜–

I'm truly sorry for breaking some of the lookups and petpages out there, and I hope this tool helps folks migrate to the new version quickly and easily! ๐Ÿ™

); } function OutfitUrlConverter() { return ( Convert an image Convert a lookup/petpage ); } function SingleImageConverter() { const [inputUrl, setInputUrl] = React.useState(""); let parsedUrl; let parseError; try { parsedUrl = parseS3OutfitUrl(inputUrl); } catch (e) { parseError = e; } const outfitId = parsedUrl?.outfitId; const size = parsedUrl?.size; const { loading, error: gqlError, data } = useQuery( gql` query OutfitUrlsSingleImageConverter( $outfitId: ID! $size: OutfitImageSize ) { outfit(id: $outfitId) { id imageUrl(size: $size) } } `, { variables: { outfitId, size: `SIZE_${size}` }, skip: outfitId == null || size == null, onError: (e) => console.error(e), } ); const imageUrl = data?.outfit?.imageUrl || ""; const previewBackground = useColorModeValue("gray.200", "whiteAlpha.300"); const spinnerSize = useBreakpointValue({ base: "md", md: "sm" }); const { onCopy, hasCopied } = useClipboard(imageUrl); return ( Enter an outfit image URL setInputUrl(e.target.value)} /> {parseError?.message || (gqlError && `Error loading outfit data. Try again?`) || null} Then, use this new URL in your layouts instead: {imageUrl && ( )}
{loading ? ( ) : imageUrl ? ( ) : null}
); } function BulkImageConverter() { const [inputHtml, setInputHtml] = React.useState(""); const parsedUrls = parseManyS3OutfitUrlsFromHtml(inputHtml); const outfitIds = parsedUrls.map((pu) => pu.outfitId); // TODO: Do this query in batches for large pages? const { loading, error, data } = useQuery( gql` query OutfitUrlsBulkImageConverter($outfitIds: [ID!]!) { outfits(ids: $outfitIds) { id # Rather than send requests for different sizes separately, I'm just # requesting them all and having the client choose, to simplify the # query. gzip should compress it very efficiently! imageUrl600: imageUrl(size: SIZE_600) imageUrl300: imageUrl(size: SIZE_300) imageUrl150: imageUrl(size: SIZE_150) } } `, { variables: { outfitIds }, skip: outfitIds.length === 0, onError: (e) => console.error(e), } ); const { outputHtml, numReplacements, replacementErrors } = React.useMemo( () => inputHtml && data?.outfits ? replaceS3OutfitUrlsInHtml(inputHtml, data.outfits) : { outputHtml: "", numReplacements: 0, replacementErrors: [] }, [inputHtml, data?.outfits] ); React.useEffect(() => { for (const replacementError of replacementErrors) { console.error("Error replacing outfit URLs in HTML:", replacementError); } }, [replacementErrors]); const { onCopy, hasCopied } = useClipboard(outputHtml); return ( Enter your lookup/petpage HTML