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 ,
2021-05-25 05:28:02 -07:00
Popover ,
PopoverArrow ,
PopoverBody ,
PopoverContent ,
PopoverTrigger ,
2021-05-25 03:35:32 -07:00
Tab ,
TabList ,
TabPanel ,
TabPanels ,
Tabs ,
2021-05-25 04:04:35 -07:00
Textarea ,
2021-05-25 03:35:32 -07:00
useClipboard ,
useColorModeValue ,
VStack ,
} from "@chakra-ui/react" ;
2021-05-24 21:07:30 -07:00
2021-05-26 20:00:01 -07:00
import { ErrorMessage , Heading1 , Heading2 , usePageTitle } from "./util" ;
2021-05-25 05:28:02 -07:00
import { CheckIcon , WarningIcon } from "@chakra-ui/icons" ;
2021-05-24 21:07:30 -07:00
function OutfitUrlsPage ( ) {
2021-05-26 19:03:21 -07:00
usePageTitle ( "Changing our outfit URLs" ) ;
2021-05-24 21:07:30 -07:00
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
2021-06-01 17:29:13 -07:00
$30 / month ! 🙏
< / p >
< p >
This change applies to < strong > image embeds < / s t r o n g > , f o r { " " }
< code > img < / c o d e > t a g s i n y o u r l o o k u p s a n d p e t p a g e s . O t h e r k i n d s o f
outfit links will stay the same !
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
2021-06-01 17:29:13 -07:00
altogether , we ' re still paying $30 / month < em > just < / e m > t o s u p p o r t
2021-05-24 21:07:30 -07:00
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
2021-06-01 17:29:13 -07:00
$30 / month could be better spent another way 😖
2021-05-24 21:07:30 -07:00
< / p >
2021-05-25 05:46:10 -07:00
< p >
We haven ' t removed these images from Amazon yet , so old image URLs
will continue to work until < strong > at least July 1 , 2021 < / s t r o n g > !
Then , we ' ll replace the old images with a short message and a link
to this page , so it ' s easy to learn what happened and fix things up .
< / p >
2021-05-24 21:07:30 -07:00
< 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 >
2021-05-25 05:46:10 -07:00
< p >
Thanks again everyone for your constant support , we appreciate you
so so much ! ! 💖 And please let me know at { " " }
< a href = "mailto:matchu@openneo.net" > matchu @ openneo . net < / a > w i t h a n y
thoughts you have — it ' s always great to hear from you , and it always
make things better 💕
< / p >
2021-05-24 21:07:30 -07:00
< / 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 ( "" ) ;
2021-05-26 19:52:57 -07:00
let outputUrl ;
2021-05-25 03:35:32 -07:00
let parseError ;
try {
2021-05-26 19:52:57 -07:00
outputUrl = convertS3OutfitUrl ( inputUrl ) ;
2021-05-25 03:35:32 -07:00
} catch ( e ) {
parseError = e ;
}
2021-06-01 17:29:13 -07:00
const isAlreadyConverted = parseError instanceof UrlAlreadyConvertedError ;
const isInvalid = parseError && ! isAlreadyConverted ;
if ( isAlreadyConverted ) {
outputUrl = inputUrl ;
}
const previewImageUrl = isAlreadyConverted
? buildNewOutfitUrl ( { outfitId : parseError . outfitId , size : 300 } )
: outputUrl ? . endsWith ( "png" )
? outputUrl
: null ;
2021-05-25 03:35:32 -07:00
2021-06-01 17:29:13 -07:00
const previewBackground = useColorModeValue ( "gray.200" , "whiteAlpha.300" ) ;
2021-05-26 19:52:57 -07:00
const { onCopy , hasCopied } = useClipboard ( outputUrl ) ;
2021-05-25 03:35:32 -07:00
return (
< Grid
templateAreas = { {
base : `
"input"
"output"
"preview"
` ,
md : `
"preview input"
"preview output"
` ,
} }
templateColumns = { { base : "auto" , md : "auto 1fr" } }
columnGap = "4"
rowGap = "2"
justifyItems = "center"
>
2021-06-01 17:29:13 -07:00
< FormControl gridArea = "input" isInvalid = { isInvalid } >
2021-05-25 03:35:32 -07:00
< 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 ) }
/ >
2021-05-26 19:52:57 -07:00
< FormErrorMessage > { parseError ? . message || null } < / F o r m E r r o r M e s s a g e >
2021-05-25 03:35:32 -07:00
< / F o r m C o n t r o l >
< FormControl gridArea = "output" >
2021-06-01 17:29:13 -07:00
< Flex marginBottom = "2" >
< FormLabel fontSize = "sm" margin = "0" >
Then , use this new URL in your layouts instead :
< / F o r m L a b e l >
< Box flex = "1 0 auto" width = "2" / >
{ isAlreadyConverted && (
< Flex alignItems = "center" fontSize = "sm" opacity = "0.8" >
< CheckIcon marginRight = "1.5" / >
< Box > { parseError . message } < / B o x >
< / F l e x >
) }
< / F l e x >
2021-05-25 03:35:32 -07:00
< InputGroup size = "sm" >
< Input
2021-05-26 19:52:57 -07:00
placeholder = "https://impress-outfit-images.openneo.net/outfits/123456789/600.png"
2021-05-25 03:35:32 -07:00
isReadOnly
2021-06-01 17:29:13 -07:00
value = { outputUrl || "" }
2021-05-25 03:35:32 -07:00
/ >
2021-05-26 19:52:57 -07:00
{ outputUrl && (
2021-05-25 03:35:32 -07:00
< 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" } }
2021-05-25 05:47:58 -07:00
maxHeight = { { base : "300px" , md : "150px" } }
2021-05-25 03:35:32 -07:00
ratio = { 1 }
background = { previewBackground }
borderRadius = "md"
boxShadow = "sm"
marginTop = { { base : "4" , md : "0" } }
overflow = "hidden"
>
< Center >
2021-06-01 17:29:13 -07:00
{ previewImageUrl && (
2021-05-25 03:35:32 -07:00
< Box
as = "img"
2021-06-01 17:29:13 -07:00
src = { previewImageUrl }
2021-05-25 03:35:32 -07:00
alt = "Outfit image preview"
maxWidth = "100%"
maxHeight = "100%"
sx = { {
// Don't let alt text flash in while loading
"&:-moz-loading" : {
visibility : "hidden" ,
} ,
} }
/ >
2021-05-26 19:52:57 -07:00
) }
2021-05-25 03:35:32 -07:00
< / 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 ( "" ) ;
2021-05-26 20:00:01 -07:00
const { outputHtml , numReplacements } = React . useMemo (
2021-05-25 05:28:02 -07:00
( ) =>
2021-05-26 20:00:01 -07:00
inputHtml
? replaceS3OutfitUrlsInHtml ( inputHtml )
: { outputHtml : "" , numReplacements : 0 } ,
[ inputHtml ]
2021-05-25 05:28:02 -07:00
) ;
2021-05-25 04:04:35 -07:00
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 05:28:02 -07:00
< Grid
templateAreas = { {
base : `
"header"
"textarea"
"status"
` ,
md : `
"header status"
"textarea textarea"
` ,
} }
alignItems = "center"
rowGap = "2"
>
< Flex gridArea = "header" alignItems = "center" >
< FormLabel fontSize = "sm" margin = "0" >
Then , use this new HTML for your page instead :
< / F o r m L a b e l >
< Box width = "2" flex = { { base : "1 0 auto" , sm : "0 0 auto" } } / >
{ 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 >
) }
< / F l e x >
< Textarea
gridArea = "textarea"
isReadOnly
fontFamily = "monospace"
fontSize = "xs"
placeholder = { ` <table> <!-- Example output, your new HTML will appear here! -->
2021-05-25 04:17:09 -07:00
< 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 05:28:02 -07:00
value = { outputHtml }
/ >
< Box gridArea = "status" textAlign = "right" justifySelf = "end" >
2021-05-26 20:00:01 -07:00
{ outputHtml && numReplacements === 0 ? (
2021-05-25 05:28:02 -07:00
< Popover trigger = "hover" >
< PopoverTrigger >
< Flex
as = { ErrorMessage }
alignItems = "center"
fontSize = "sm"
tabIndex = "0"
borderRadius = "md"
paddingX = "2"
marginRight = "-2"
_focus = { { outline : "0" , boxShadow : "outline" } }
>
< WarningIcon marginRight = "1.5" / >
< Box > No outfit image URLs found < / B o x >
< / F l e x >
< / P o p o v e r T r i g g e r >
< PopoverContent >
< PopoverArrow / >
< PopoverBody >
< Box fontSize = "xs" textAlign = "center" >
< b > Make sure they ' re in the right format : < / b >
< br / >
https : //openneo-uploads.s3.amazonaws.com/outfits/123/456/789/preview.png
< br / >
< br / >
< b >
If they 're already in the new format, then you' re
already done !
< / b > { " " }
The new format is :
< br / >
https : //impress-outfit-images.openneo.net/outfits/123456789/v/1020304050/600.png
< / B o x >
< / P o p o v e r B o d y >
< / P o p o v e r C o n t e n t >
< / P o p o v e r >
) : outputHtml ? (
< Flex alignItems = "center" fontSize = "sm" opacity = "0.8" >
< CheckIcon marginRight = "1.5" / >
< Box > Converted { numReplacements } outfit images ! < / B o x >
< / F l e x >
) : null }
< / B o x >
< / G r i d >
2021-05-25 04:04:35 -07:00
< / F o r m C o n t r o l >
< / G r i d >
) ;
2021-05-25 03:35:32 -07:00
}
2021-05-25 05:28:02 -07:00
// These patterns have the same content, but different boundary conditions and
// flags. EXACT is for checking a single string for an exact match, GLOBAL is
// for finding multiple matches in large text.
const S3 _OUTFIT _URL _EXACT _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 _OUTFIT _URL _GLOBAL _PATTERN = /https?:\/\/openneo-uploads\.s3\.amazonaws\.com\/outfits\/([0-9]{3})\/([0-9]{3})\/([0-9]{3})\/(preview|medium_preview|small_preview)\.png/g ;
2021-06-01 17:29:13 -07:00
const OUTFIT _PAGE _URL _EXACT _PATTERN = /^https?:\/\/impress(-2020)?\.openneo\.net\/outfits\/([0-9]+)(\?.*)?$/ ;
2021-05-25 03:35:32 -07:00
const S3 _FILENAMES _TO _SIZES = {
preview : 600 ,
medium _preview : 300 ,
small _preview : 150 ,
} ;
function parseS3OutfitUrl ( url ) {
if ( ! url ) {
return null ;
}
2021-06-01 17:29:13 -07:00
const outfitPageMatch = url . match ( OUTFIT _PAGE _URL _EXACT _PATTERN ) ;
if ( outfitPageMatch ) {
throw new UrlAlreadyConvertedError (
` Outfit page links don't need to change! ` ,
outfitPageMatch [ 2 ]
) ;
}
const match = url . match ( S3 _OUTFIT _URL _EXACT _PATTERN ) ;
2021-05-25 03:35:32 -07:00
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-06-01 17:29:13 -07:00
function buildNewOutfitUrl ( { outfitId , size } ) {
return ` https://impress-outfit-images.openneo.net/outfits/ ${ outfitId } / ${ size } .png ` ;
}
2021-05-26 19:52:57 -07:00
function convertS3OutfitUrl ( url ) {
const parsedUrl = parseS3OutfitUrl ( url ) ;
if ( ! parsedUrl ) {
return null ;
}
2021-06-01 17:29:13 -07:00
return buildNewOutfitUrl ( parsedUrl ) ;
2021-05-26 19:52:57 -07:00
}
2021-05-26 20:00:01 -07:00
function replaceS3OutfitUrlsInHtml ( html ) {
// Use the `replace` method to scan the HTML for matches, and count the
// replacements as we go!
2021-05-25 05:28:02 -07:00
let numReplacements = 0 ;
const outputHtml = html . replace ( S3 _OUTFIT _URL _GLOBAL _PATTERN , ( match ) => {
numReplacements ++ ;
2021-05-26 20:00:01 -07:00
return convertS3OutfitUrl ( match ) ;
2021-05-25 05:28:02 -07:00
} ) ;
2021-05-26 20:00:01 -07:00
return { outputHtml , numReplacements } ;
2021-05-25 05:28:02 -07:00
}
2021-06-01 17:29:13 -07:00
class UrlAlreadyConvertedError extends Error {
constructor ( message , outfitId ) {
super ( message ) ;
this . outfitId = outfitId ;
}
}
2021-05-24 21:07:30 -07:00
export default OutfitUrlsPage ;