import React from "react"; import { css } from "@emotion/react"; import { AspectRatio, Box, Button, Center, FormControl, FormErrorMessage, FormLabel, Grid, Input, InputGroup, InputRightElement, Tab, TabList, TabPanel, TabPanels, Tabs, useBreakpointValue, useClipboard, useColorModeValue, VStack, } from "@chakra-ui/react"; import { Delay, Heading1, Heading2 } from "./util"; import HangerSpinner from "./components/HangerSpinner"; import { gql, useQuery } from "@apollo/client"; 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() { return TODO: Bulk image converter; } const S3_OUTFIT_URL_PATTERN = /^https?:\/\/openneo-uploads\.s3\.amazonaws\.com\/outfits\/([0-9]{3})\/([0-9]{3})\/([0-9]{3})\/(preview|medium_preview|small_preview)\.png$/; const S3_FILENAMES_TO_SIZES = { preview: 600, medium_preview: 300, small_preview: 150, }; function parseS3OutfitUrl(url) { if (!url) { return null; } const match = S3_OUTFIT_URL_PATTERN.exec(url); if (!match) { throw new Error( `This URL didn't match the expected pattern. Make sure it's formatted like this: https://openneo-uploads.s3.amazonaws.com/outfits/123/456/789/preview.png` ); } // Convert ID to number to remove leading 0s, then convert back to string for // consistency with how we handle outfit IDs in this app. const outfitId = String(Number(`${match[1]}${match[2]}${match[3]}`)); const size = S3_FILENAMES_TO_SIZES[match[4]]; return { outfitId, size }; } export default OutfitUrlsPage;