2021-05-24 21:07:30 -07:00
import React from "react" ;
import { css } from "@emotion/react" ;
2021-05-25 03:35:32 -07:00
import {
AspectRatio ,
Box ,
Button ,
Center ,
2021-05-25 04:04:35 -07:00
Flex ,
2021-05-25 03:35:32 -07:00
FormControl ,
FormErrorMessage ,
FormLabel ,
Grid ,
Input ,
InputGroup ,
InputRightElement ,
Tab ,
TabList ,
TabPanel ,
TabPanels ,
Tabs ,
2021-05-25 04:04:35 -07:00
Textarea ,
2021-05-25 03:35:32 -07:00
useBreakpointValue ,
useClipboard ,
useColorModeValue ,
VStack ,
} from "@chakra-ui/react" ;
2021-05-24 21:07:30 -07:00
2021-05-25 03:35:32 -07:00
import { Delay , Heading1 , Heading2 } from "./util" ;
import HangerSpinner from "./components/HangerSpinner" ;
import { gql , useQuery } from "@apollo/client" ;
2021-05-24 21:07:30 -07:00
function OutfitUrlsPage ( ) {
return (
< >
< Heading1 marginBottom = "4" > Changing our outfit URLs < / H e a d i n g 1 >
< VStack
spacing = "4"
alignItems = "flex-start"
css = { css `
max - width : 800 px ;
p {
margin - bottom : 1 em ;
}
a {
text - decoration : underline ;
}
h2 ,
h3 {
margin - bottom : 0.5 em ;
}
` }
>
< section >
< p >
2021-05-25 03:35:32 -07:00
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 ! 🙏
2021-05-24 21:07:30 -07:00
< / p >
2021-05-25 03:35:32 -07:00
< OutfitUrlConverter / >
2021-05-24 21:07:30 -07:00
< / s e c t i o n >
2021-05-25 03:35:32 -07:00
< Box height = "2" / >
2021-05-24 21:07:30 -07:00
< section >
< Heading2 > The history < / H e a d i n g 2 >
< p >
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 { " " }
< a href = "https://aws.amazon.com/s3/" > Amazon S3 < / a > , a n d g a v e y o u
Amazon ' s URL for them , at < code > amazonaws . com < / c o d e > .
< / p >
< p >
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 < em > store < / e m > o u r m i l l i o n s o f
outfit images , including the ones nobody visits 😅
< / p >
< p >
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 < em > just < / e m > t o s u p p o r t
the old < code > amazonaws . com < / c o d e > U R L s .
< / p >
< p >
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 😖
< / p >
< p >
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 ! 🙏
< / p >
< / s e c t i o n >
< / V S t a c k >
< / >
) ;
}
2021-05-25 03:35:32 -07:00
function OutfitUrlConverter ( ) {
return (
< Tabs >
< TabList >
< Tab > Convert an image < / T a b >
< Tab > Convert a lookup / petpage < / T a b >
< / T a b L i s t >
< TabPanels >
< TabPanel >
< SingleImageConverter / >
< / T a b P a n e l >
< TabPanel >
< BulkImageConverter / >
< / T a b P a n e l >
< / T a b P a n e l s >
< / T a b s >
) ;
}
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 (
< Grid
templateAreas = { {
base : `
"input"
"output"
"preview"
` ,
md : `
"preview input"
"preview output"
` ,
} }
templateColumns = { { base : "auto" , md : "auto 1fr" } }
columnGap = "4"
rowGap = "2"
justifyItems = "center"
>
< FormControl gridArea = "input" isInvalid = { Boolean ( parseError ) || gqlError } >
< FormLabel fontWeight = "bold" > Enter an outfit image URL < / F o r m L a b e l >
< Input
placeholder = "https://openneo-uploads.s3.amazonaws.com/outfits/123/456/789/preview.png"
value = { inputUrl }
onChange = { ( e ) => setInputUrl ( e . target . value ) }
/ >
< FormErrorMessage >
{ parseError ? . message ||
( gqlError && ` Error loading outfit data. Try again? ` ) ||
null }
< / F o r m E r r o r M e s s a g e >
< / F o r m C o n t r o l >
< FormControl gridArea = "output" >
< FormLabel fontSize = "sm" >
Then , use this new URL in your layouts instead :
< / F o r m L a b e l >
< InputGroup size = "sm" >
< Input
placeholder = "https://impress-outfit-images.openneo.net/outfits/123456789/v/1020304050/600.png"
isReadOnly
value = { imageUrl }
/ >
{ imageUrl && (
< InputRightElement width = "4rem" paddingRight = "1" >
< Button
height = "calc(100% - .5rem)"
size = "xs"
minWidth = "100%"
onClick = { onCopy }
>
{ hasCopied ? "Copied!" : "Copy" }
< / B u t t o n >
< / I n p u t R i g h t E l e m e n t >
) }
< / I n p u t G r o u p >
< / F o r m C o n t r o l >
< AspectRatio
gridArea = "preview"
width = { { base : "100%" , md : "150px" } }
maxWidth = { { base : "300px" , md : "150px" } }
ratio = { 1 }
background = { previewBackground }
borderRadius = "md"
boxShadow = "sm"
marginTop = { { base : "4" , md : "0" } }
overflow = "hidden"
>
< Center >
{ loading ? (
< Delay ms = { 1000 } >
< HangerSpinner size = { spinnerSize } / >
< / D e l a y >
) : imageUrl ? (
< Box
as = "img"
src = { imageUrl }
alt = "Outfit image preview"
width = { size }
height = { size }
maxWidth = "100%"
maxHeight = "100%"
sx = { {
// Don't let alt text flash in while loading
"&:-moz-loading" : {
visibility : "hidden" ,
} ,
} }
/ >
) : null }
< / C e n t e r >
< / A s p e c t R a t i o >
< / G r i d >
) ;
}
function BulkImageConverter ( ) {
2021-05-25 04:04:35 -07:00
const [ inputHtml , setInputHtml ] = React . useState ( "" ) ;
const outputHtml = inputHtml
? "<!-- TODO: Not implemented yet! -->\n" + inputHtml
: "" ;
const { onCopy , hasCopied } = useClipboard ( outputHtml ) ;
return (
< Grid
templateAreas = { `
"input"
"output"
` }
rowGap = "4"
>
< FormControl gridArea = "input" >
< FormLabel fontWeight = "bold" > Enter your lookup / petpage HTML < / F o r m L a b e l >
< Textarea
fontFamily = "monospace"
fontSize = "xs"
2021-05-25 04:17:09 -07:00
placeholder = { ` <table> <!-- Example input, paste your HTML here! -->
< tr >
< td > < img src = "https://openneo-uploads.s3.amazonaws.com/outfits/123/456/700/preview.png" > < / t d >
< td > < img src = "https://openneo-uploads.s3.amazonaws.com/outfits/123/456/701/preview.png" > < / t d >
< td > < img src = "https://openneo-uploads.s3.amazonaws.com/outfits/123/456/702/preview.png" > < / t d >
... ` }
2021-05-25 04:04:35 -07:00
value = { inputHtml }
onChange = { ( e ) => setInputHtml ( e . target . value ) }
/ >
< / F o r m C o n t r o l >
< FormControl gridArea = "output" >
2021-05-25 04:17:09 -07:00
< Flex alignItems = "center" marginBottom = "2" >
< FormLabel fontSize = "sm" margin = "0" >
2021-05-25 04:04:35 -07:00
Then , use this new HTML for your page instead :
< / F o r m L a b e l >
2021-05-25 04:17:09 -07:00
< Box width = "2" / >
{ outputHtml && (
< Button size = "xs" onClick = { onCopy } >
< Grid templateAreas = "the-area" >
< Box gridArea = "the-area" > { hasCopied ? "Copied!" : "Copy" } < / B o x >
{ / * T h i s i n v i s i b l e " C o p i e d ! " e n f o r c e s a m i n s i z e f o r t h e b u t t o n
* content , so that the button never changes size . * / }
< Box gridArea = "the-area" aria - hidden visibility = "hidden" >
Copied !
< / B o x >
< / G r i d >
< / B u t t o n >
) }
2021-05-25 04:04:35 -07:00
< / F l e x >
< Textarea
isReadOnly
fontFamily = "monospace"
fontSize = "xs"
2021-05-25 04:17:09 -07:00
placeholder = { ` <table> <!-- Example output, your new HTML will appear here! -->
< tr >
< td > < img src = "https://impress-outfit-images.openneo.net/outfits/123456700/v/1234/600.png" > < / t d >
< td > < img src = "https://impress-outfit-images.openneo.net/outfits/123456701/v/5678/600.png" > < / t d >
< td > < img src = "https://impress-outfit-images.openneo.net/outfits/123456702/v/9012/600.png" > < / t d >
... ` }
2021-05-25 04:04:35 -07:00
value = { outputHtml }
/ >
< / F o r m C o n t r o l >
< / G r i d >
) ;
2021-05-25 03:35:32 -07:00
}
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 } ;
}
2021-05-24 21:07:30 -07:00
export default OutfitUrlsPage ;