├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── patches └── image-butter+1.0.3.patch ├── public ├── _redirects ├── favicon.svg ├── heart.png ├── index.html ├── instagram.png ├── like.png ├── logos │ ├── email.svg │ ├── facebook-ads.svg │ ├── facebook.svg │ ├── google.svg │ ├── instagram.svg │ ├── linkedin.svg │ ├── open-graph.svg │ ├── pinterest.svg │ ├── snapchat.svg │ ├── tiktok.svg │ ├── twitter.svg │ ├── website.svg │ └── youtube.svg ├── manifest.json ├── positions │ ├── facebook-cover-photo.svg │ ├── facebook-image-post.svg │ ├── facebook-profile-picture.svg │ ├── instagram-profile-picture.svg │ ├── instagram-square-post.svg │ ├── instagram-stories.svg │ ├── twitter-header.svg │ ├── twitter-post-image.svg │ ├── twitter-profile-photo.svg │ ├── youtube-channel-art.svg │ ├── youtube-profile-picture.svg │ └── youtube-thumbnail.svg └── robots.txt ├── src ├── App.tsx ├── app.module.css ├── components │ ├── button │ │ ├── button.module.css │ │ └── index.tsx │ ├── container │ │ ├── container.module.css │ │ └── index.tsx │ ├── poweredBy │ │ ├── index.tsx │ │ ├── mv.png │ │ ├── poweredBy.module.css │ │ └── uc.svg │ ├── size │ │ ├── index.tsx │ │ ├── size.module.css │ │ └── static.gif │ ├── tabs │ │ ├── index.tsx │ │ └── tabs.module.css │ └── target │ │ ├── index.tsx │ │ └── target.module.css ├── global.css ├── helpers.ts ├── index.tsx ├── react-app-env.d.ts ├── reportWebVitals.ts ├── sizes.d.ts ├── sizes.json ├── types.ts └── vendors │ ├── cookie-consent-js.js │ ├── image-zoom.d.ts │ ├── image-zoom.js │ ├── use-image-zoom.d.ts │ └── use-image-zoom.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Attributions 2 | 3 | - Website icon by [Karacis Studios](https://thenounproject.com/karacis) 4 | - Email icon by [Rainbow Designs](https://thenounproject.com/iahmadali26) 5 | - Open Graph icon by [Heroicons](https://heroicons.com) 6 | - All other brand icons by [Simple Icons](https://simpleicons.org) 7 | - Cat demo image by [Cédric VT](https://unsplash.com/@cedric_photography) 8 | - Glow gif by [Erica Anderson](https://giphy.com/gifs/trippy-gif-artist-ericaofanderson-2aQS3AHfvvfIkSdbFM) 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixelhunter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@rpearce/ts-dom-fns": "^0.4.0", 7 | "@types/lodash": "^4.14.170", 8 | "@types/node": "^12.20.14", 9 | "@types/react": "^17.0.9", 10 | "@types/react-dom": "^17.0.6", 11 | "@types/react-transition-group": "^4.4.1", 12 | "@types/vanilla-tilt": "^1.6.2", 13 | "@uploadcare/react-widget": "^1.3.7", 14 | "detect-browser": "^5.2.0", 15 | "focus-options-polyfill": "^1.5.0", 16 | "image-butter": "^1.0.3", 17 | "jszip": "^3.6.0", 18 | "lodash": "^4.17.21", 19 | "react": "^17.0.2", 20 | "react-dom": "^17.0.2", 21 | "react-icons": "^4.2.0", 22 | "react-scripts": "4.0.3", 23 | "react-transition-group": "^4.4.2", 24 | "typescript": "^4.3.2", 25 | "vanilla-tilt": "^1.7.0", 26 | "web-vitals": "^1.1.2" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "exit 0", 32 | "postinstall": "patch-package" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "patch-package": "^6.4.7" 53 | }, 54 | "proxy": "https://ucarecdn.com/" 55 | } 56 | -------------------------------------------------------------------------------- /patches/image-butter+1.0.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/image-butter/src/handleImages.js b/node_modules/image-butter/src/handleImages.js 2 | index 6f025d7..dd0096b 100644 3 | --- a/node_modules/image-butter/src/handleImages.js 4 | +++ b/node_modules/image-butter/src/handleImages.js 5 | @@ -6,7 +6,7 @@ export default cb => { 6 | const imagesLoaded = imagesArray.map(() => false) 7 | 8 | const queue = new Metameta({ 9 | - interval: config.appearDuration, 10 | + interval: 100, 11 | bias: 2 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* https://ucarecdn.com/:splat 200 2 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miloxeon/pixelhunter/011932f0c6d42f41d6fe43794c9abd94935965f8/public/heart.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 27 | Pixelhunter — 🧠 AI-powered image resizer for social media 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/instagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miloxeon/pixelhunter/011932f0c6d42f41d6fe43794c9abd94935965f8/public/instagram.png -------------------------------------------------------------------------------- /public/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miloxeon/pixelhunter/011932f0c6d42f41d6fe43794c9abd94935965f8/public/like.png -------------------------------------------------------------------------------- /public/logos/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/facebook-ads.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/open-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/pinterest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/snapchat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/website.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/logos/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Pixelhunter", 3 | "name": "Pixelhunter", 4 | "icons": [{ 5 | "src": "favicon.ico", 6 | "sizes": "64x64 32x32 24x24 16x16", 7 | "type": "image/x-icon" 8 | }, { 9 | "src": "logo192.png", 10 | "type": "image/png", 11 | "sizes": "192x192" 12 | }, { 13 | "src": "logo512.png", 14 | "type": "image/png", 15 | "sizes": "512x512" 16 | }], 17 | "start_url": ".", 18 | "display": "standalone", 19 | "theme_color": "#000000", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/positions/facebook-cover-photo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/facebook-image-post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/facebook-profile-picture.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/instagram-profile-picture.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/instagram-square-post.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/instagram-stories.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/twitter-header.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/twitter-post-image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/twitter-profile-photo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/youtube-channel-art.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/youtube-profile-picture.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/positions/youtube-thumbnail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FileInfo, Widget as UploadcareUpload } from '@uploadcare/react-widget' 3 | import { CSSTransition } from 'react-transition-group' 4 | import { AiOutlineLoading3Quarters } from 'react-icons/ai' 5 | import { FiArrowDown } from 'react-icons/fi' 6 | import { downloadSizes, mimeToExtension, getSimpleModeSizes } from './helpers' 7 | import { SizeWithSrc, UCMeta } from './types' 8 | import { detect } from 'detect-browser' 9 | import sizes from './sizes.json' 10 | import css from './app.module.css' 11 | 12 | import Button from './components/button' 13 | import Container from './components/container' 14 | import Target from './components/target' 15 | import Tabs, { TabsEnum } from './components/tabs' 16 | import PoweredBy from './components/poweredBy' 17 | 18 | const timeouts = { appear: 2000, enter: 500, exit: 500 } 19 | const simpleModeImages = getSimpleModeSizes(sizes) 20 | const advancedModeLogos = sizes.map(target => ({ 21 | logoSrc: target.logoSrc, 22 | app: target.app, 23 | })) 24 | 25 | const sizesCount = sizes.map(target => target.sizes.length).reduce((a, b) => a + b, 0) 26 | const sizePlural = sizesCount.toString() === '1' ? 'size' : 'sizes' 27 | const browserInfo = detect() 28 | const isSafari = !browserInfo || browserInfo.name === 'safari' || browserInfo.name === 'ios' 29 | 30 | const App: React.FC = () => { 31 | // bright 32 | // const [src, setSrc] = React.useState('https://ucarecdn.com/15f79d17-2619-46e6-b120-8fc7f58f50a4/') 33 | 34 | // dim 35 | // const [src, setSrc] = React.useState('https://ucarecdn.com/0c17d734-460a-4d79-9824-30b6e6378181/') 36 | 37 | // demo (cat on blue background) 38 | const [src, setSrc] = React.useState('https://ucarecdn.com/b555ca32-eee0-48cf-a943-175bc1498367/') 39 | 40 | const [loading, setLoading] = React.useState(false) 41 | const [activeTab, setActiveTab] = React.useState(TabsEnum.simple) 42 | 43 | const [ucMeta, setUcMeta] = React.useState({ 44 | compress: false, 45 | extension: 'jpg', 46 | }) 47 | 48 | const uploadOnChange = React.useCallback( 49 | (fileInfo: FileInfo) => { 50 | setSrc(fileInfo.cdnUrl) 51 | setUcMeta({ 52 | ...ucMeta, 53 | extension: mimeToExtension(fileInfo.mimeType), 54 | }) 55 | }, 56 | [ucMeta], 57 | ) 58 | 59 | // demo 60 | const downloadAll = React.useCallback(() => { 61 | if (!src) return 62 | 63 | const sizesToDownload = Array.from(document.querySelectorAll('[data-checkbox]:checked')) 64 | .map(node => { 65 | const checkbox = node as HTMLInputElement 66 | const { app, name, width, height } = checkbox.dataset 67 | if (!app || !name || !width || !height) return null 68 | 69 | return { 70 | app, 71 | name, 72 | width: parseInt(width, 10), 73 | height: parseInt(height, 10), 74 | src, 75 | } 76 | }) 77 | .filter(Boolean) as SizeWithSrc[] 78 | 79 | if (sizesToDownload.length === 0) return 80 | 81 | setLoading(true) 82 | downloadSizes(sizesToDownload, ucMeta).finally(() => setLoading(false)) 83 | }, [ucMeta, src]) 84 | 85 | return ( 86 | <> 87 | 88 |
89 |
90 | {!isSafari ? ( 91 |

92 | Pixel­hunter — free AI image resizer for social media. 93 |

94 | ) : ( 95 |

Pixel­hunter — free AI image resizer for social media.

96 | )} 97 | 98 |

99 | Cropping each and every image by hand can be tiresome. Pixelhunter utilizes amazing Uploadcare Intelligence API to recognize objects and crop pictures automatically, in a smarter way. 100 |

101 |

102 | Just upload your image of any size and it will be automatically resized to each and every of{' '} 103 | 104 | {sizesCount} {sizePlural} 105 | {' '} 106 | we support. AI is there to ensure that your image is resized in the best way that a robot can do. 107 |

108 |

109 | Other than that, Pixelhunter features real pro-tips that are there to actually help you and not just to fill up the visual space. 110 |

111 | 112 | 113 | 114 |
115 |
116 | 117 |
118 | 119 |
120 |

We support:

121 |
122 | {advancedModeLogos.map(appInfo => { 123 | return {`Images 124 | })} 125 |
126 |
127 |
128 | {src !== null && } 129 |
130 |
131 |
132 | 133 |
134 |
135 | 136 |
137 |
138 | 139 |
140 |
141 |
142 |
143 | 144 | {src !== null && ( 145 |
146 | {src !== null && ( 147 | 151 | )} 152 | 153 | 154 | 155 | {simpleModeImages.map(target => { 156 | return 157 | })} 158 | 159 | 160 | 161 | 162 | {sizes.map(target => { 163 | return 164 | })} 165 | 166 | 167 |
168 | )} 169 | 170 | {/* eslint-disable-next-line react/jsx-no-target-blank, jsx-a11y/img-redundant-alt */} 171 | 172 | 173 | 174 | 175 | 176 | 177 | pixelhunter@miloxeon.com 178 | 179 | 180 | 181 | ) 182 | } 183 | 184 | export default App 185 | -------------------------------------------------------------------------------- /src/app.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | position: relative; 3 | } 4 | 5 | .content { 6 | max-width: 35rem; 7 | background-color: var(--b); 8 | margin: 1rem 0 4rem -1em; 9 | padding-left: 1em; 10 | box-shadow: var(--separate); 11 | } 12 | 13 | .h1 { 14 | font-size: 4rem; 15 | text-shadow: var(--elevate-before); 16 | } 17 | 18 | .rose { 19 | background-image: radial-gradient( 20 | circle farthest-corner at 20% 200%, 21 | rgb(255, 225, 125) 0%, 22 | rgb(255, 205, 105) 10%, 23 | rgb(250, 145, 55) 28%, 24 | rgb(235, 65, 65) 42%, 25 | transparent 82% 26 | ), 27 | linear-gradient(135deg, rgb(35, 75, 215) 12%, rgb(195, 60, 190) 58%); 28 | 29 | border-radius: 1rem; 30 | background-clip: text; 31 | text-shadow: none; 32 | color: transparent; 33 | filter: var(--elevate-before-filter); 34 | } 35 | 36 | .p { 37 | font-size: large; 38 | text-shadow: var(--elevate-weak); 39 | } 40 | 41 | .upload { 42 | margin-top: 3rem; 43 | margin-bottom: 3rem; 44 | padding: 2rem; 45 | z-index: 2; 46 | 47 | border-radius: 1rem; 48 | 49 | box-shadow: var(--neumorphic); 50 | } 51 | 52 | .uploaderWrapper { 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | padding: 4rem 0; 57 | } 58 | 59 | .h2 { 60 | margin-bottom: 2rem; 61 | } 62 | 63 | .grid { 64 | display: grid; 65 | gap: 2rem; 66 | grid-template-columns: repeat(auto-fill, minmax(40px, auto)); 67 | align-items: start; 68 | justify-content: start; 69 | } 70 | 71 | .infoLogo { 72 | border-radius: 0.5em; 73 | } 74 | 75 | .background { 76 | position: absolute; 77 | top: 0; 78 | right: 0; 79 | z-index: -1; 80 | max-width: 35rem; 81 | width: 100%; 82 | 83 | display: flex; 84 | flex-direction: column; 85 | } 86 | 87 | .wrapperCommons { 88 | max-width: 20rem; 89 | } 90 | 91 | .likeImageWrapper { 92 | composes: wrapperCommons; 93 | align-self: flex-end; 94 | } 95 | 96 | .instagramImageWrapper { 97 | composes: wrapperCommons; 98 | margin-top: 4rem; 99 | display: inline-block; 100 | } 101 | 102 | .heartImageWrapper { 103 | composes: wrapperCommons; 104 | align-self: flex-end; 105 | } 106 | 107 | @media not prefers-reduced-motion { 108 | .wrapperCommons { 109 | animation: levitate 15s infinite; 110 | } 111 | 112 | .likeImageWrapper { 113 | animation-delay: 2s; 114 | animation-timing-function: var(--elevate-bezier); 115 | } 116 | 117 | .instagramImageWrapper { 118 | animation-duration: 15s, 180s; 119 | animation-name: levitate, rotate; 120 | animation-timing-function: var(--elevate-bezier), linear; 121 | } 122 | 123 | .heartImageWrapper { 124 | animation-delay: 4s; 125 | } 126 | } 127 | 128 | @keyframes levitate { 129 | 0% { 130 | transform: translateY(0); 131 | } 132 | 50% { 133 | transform: translateY(20px); 134 | } 135 | 100% { 136 | transform: translateY(0); 137 | } 138 | } 139 | 140 | @keyframes rotate { 141 | 0% { 142 | transform: rotate(0deg); 143 | } 144 | 50% { 145 | transform: rotate(180deg); 146 | } 147 | 100% { 148 | transform: rotate(360deg); 149 | } 150 | } 151 | 152 | .tabsWrapper { 153 | position: relative; 154 | overflow: hidden; 155 | } 156 | 157 | .download { 158 | position: fixed; 159 | bottom: 1rem; 160 | right: 1rem; 161 | z-index: 10; 162 | 163 | display: inline-flex; 164 | align-items: center; 165 | box-shadow: var(--elevate-before) !important; 166 | } 167 | 168 | .download[aria-busy='true'] { 169 | pointer-events: none; 170 | } 171 | 172 | .iconCommons { 173 | width: 1rem; 174 | height: 1rem; 175 | margin-right: 0.5em; 176 | } 177 | 178 | .loading { 179 | composes: iconCommons; 180 | animation: rotate 1000ms infinite linear; 181 | } 182 | 183 | .arrowDown { 184 | composes: iconCommons; 185 | transition: transform 0.5s; 186 | transition-timing-function: var(--elevate-bezier); 187 | } 188 | 189 | .download:hover .arrowDown { 190 | transform: translateY(2px); 191 | transition: transform 0.1s; 192 | } 193 | 194 | .tab:not(:global [class*='tab-']) { 195 | will-change: opacity; 196 | display: none; 197 | } 198 | 199 | /* :global .tab-appear-active { 200 | transform: translateY(20px) scale(0.97); 201 | opacity: 0; 202 | pointer-events: none; 203 | animation: 204 | appear-position 1s, 205 | appear-opacity 2s; 206 | 207 | animation-fill-mode: forwards; 208 | animation-timing-function: var(--elevate-bezier); 209 | animation-delay: .2s; 210 | } 211 | 212 | :global .tab-appear-done { 213 | transform: none; 214 | opacity: 1; 215 | } */ 216 | 217 | :global .tab-enter { 218 | position: absolute; 219 | top: 0; 220 | left: 0; 221 | right: 0; 222 | opacity: 0; 223 | } 224 | 225 | :global .tab-enter-active { 226 | opacity: 1; 227 | transition: opacity 0.5s; 228 | transition-timing-function: var(--elevate-bezier); 229 | } 230 | 231 | :global .tab-exit { 232 | opacity: 1; 233 | } 234 | 235 | :global .tab-exit-active { 236 | opacity: 0; 237 | transition: opacity 0.5s; 238 | transition-timing-function: var(--elevate-bezier); 239 | } 240 | 241 | :global .tab-exit-done { 242 | display: none; 243 | } 244 | 245 | @keyframes appear-position { 246 | from { 247 | transform: translateY(20px) scale(0.97); 248 | } 249 | to { 250 | transform: translateY(0) scale(1); 251 | } 252 | } 253 | 254 | @keyframes appear-opacity { 255 | from { 256 | opacity: 0; 257 | } 258 | to { 259 | opacity: 1; 260 | } 261 | } 262 | 263 | .a { 264 | color: var(--t); 265 | 266 | text-decoration: none; 267 | background-image: var(--link-bg); 268 | background-repeat: repeat-x; 269 | background-position: left bottom; 270 | 271 | transition: color 0.3s; 272 | } 273 | 274 | .a[href^='http']::after { 275 | content: ' ↗'; 276 | user-select: none; 277 | pointer-events: none; 278 | } 279 | 280 | .a:hover { 281 | outline: none; 282 | color: var(--a1); 283 | background-image: var(--link-bg-focus); 284 | transition-duration: 0s; 285 | } 286 | 287 | .a:focus { 288 | outline: 2px solid var(--a1); 289 | outline-offset: 2px; 290 | color: var(--a1); 291 | background-image: none; 292 | transition-duration: 0s; 293 | } 294 | -------------------------------------------------------------------------------- /src/components/button/button.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | background-color: var(--b-weaker); 3 | border-radius: .3em; 4 | border: 1px solid var(--t-weaker); 5 | box-shadow: var(--shadow); 6 | color: var(--t-weak); 7 | font-weight: 500; 8 | padding: .4em .8em; 9 | 10 | transition-property: color, border-color, background-color, box-shadow; 11 | transition-duration: .1s; 12 | transition-timing-function: ease-in-out; 13 | } 14 | 15 | .root:hover { 16 | color: var(--t); 17 | border-color: var(--t-weak); 18 | } 19 | 20 | .root:focus, 21 | .root:active { 22 | outline: none; 23 | box-shadow: 0 0 0 3px var(--a1-translucent); 24 | border-color: transparent; 25 | } 26 | 27 | .accent { 28 | composes: root; 29 | color: var(--a1-c); 30 | background-color: var(--a1); 31 | border-color: var(--a1); 32 | box-shadow: none; 33 | } 34 | 35 | .accent:hover { 36 | color: var(--a1-c); 37 | border-color: var(--a1-strong); 38 | background-color: var(--a1-strong); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import css from './button.module.css' 3 | 4 | const Button: React.FC> = props => ( 5 | 96 | 106 | {/* */} 116 | 117 |
info.app).join(', ')}`} 119 | className={css.simpleModeInfo} 120 | aria-hidden={true} 121 | > 122 | {simpleModeLogos.map(appInfo => { 123 | return ( 124 | 132 | ) 133 | })} 134 |
135 | 136 |
info.app).join(', ')}`} 138 | className={css.advancedModeInfo} 139 | aria-hidden={true} 140 | > 141 | {advancedModeLogos.map(appInfo => { 142 | return ( 143 | 151 | ) 152 | })} 153 |
154 | 155 | {/*
160 | 161 |
*/} 162 | 163 |
164 |
165 | ) 166 | } 167 | 168 | export default Tabs 169 | -------------------------------------------------------------------------------- /src/components/tabs/tabs.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | position: relative; 3 | padding: .5em 0; 4 | display: grid; 5 | max-width: 35rem; 6 | gap: 1em; 7 | grid-template-columns: max-content auto; 8 | align-items: start; 9 | background-color: var(--b); 10 | grid-template-areas: 11 | "simpleTab simpleInfo" 12 | "advancedTab advancedInfo"; 13 | /* "customTab customInfo"; */ 14 | } 15 | 16 | .tab:nth-child(1) { grid-area: simpleTab; } 17 | .tab:nth-child(2) { grid-area: advancedTab; } 18 | /* .tab:nth-child(3) { grid-area: customTab; } */ 19 | 20 | .infoCommons { 21 | font-size: small; 22 | padding: .5em 1em; 23 | 24 | display: grid; 25 | width: 100%; 26 | height: 100%; 27 | gap: 1em; 28 | grid-template-columns: repeat(auto-fill, minmax(20px, auto)); 29 | align-items: center; 30 | justify-content: start; 31 | } 32 | 33 | .simpleModeInfo { 34 | composes: infoCommons; 35 | grid-area: simpleInfo; 36 | } 37 | 38 | .advancedModeInfo { 39 | composes: infoCommons; 40 | grid-area: advancedInfo; 41 | } 42 | 43 | .customModeInfo { 44 | composes: infoCommons; 45 | grid-area: customInfo; 46 | grid-template-columns: auto; 47 | } 48 | 49 | .customModeInfo svg { 50 | width: 20px; 51 | height: 20px; 52 | } 53 | 54 | .infoLogo { 55 | display: inline-block; 56 | filter: grayscale() brightness(0); 57 | } 58 | 59 | .tab { 60 | position: relative; 61 | background: none; 62 | border: none; 63 | color: var(--t); 64 | font-weight: 500; 65 | width: max-content; 66 | transition: 67 | color .3s, 68 | box-shadow .3s; 69 | transition-timing-function: var(--elevate-bezier); 70 | 71 | padding: .5em 1em; 72 | border-radius: 9999px; 73 | z-index: 2; 74 | } 75 | 76 | .tab::before { 77 | display: block; 78 | content: ''; 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | right: 0; 83 | bottom: 0; 84 | border-radius: inherit; 85 | transition: box-shadow .3s; 86 | transition-timing-function: var(--elevate-bezier); 87 | z-index: -2; 88 | } 89 | 90 | .tab:not([data-active="true"]):hover::before { 91 | box-shadow: var(--neumorphic); 92 | } 93 | 94 | .tab:not([data-active="true"]):focus { 95 | box-shadow: inset 0 0 0 3px var(--a1-translucent-weaker); 96 | } 97 | 98 | .tab:focus, 99 | .tab:active { 100 | outline: none; 101 | } 102 | 103 | .tab[data-active="true"] { 104 | color: var(--a1-c); 105 | } 106 | 107 | .tab[data-active="true"]:hover ~ .pill { 108 | filter: brightness(90%); 109 | } 110 | 111 | .pill { 112 | composes: accent from '../button/button.module.css'; 113 | position: absolute; 114 | top: 0; 115 | left: 0; 116 | will-change: transform, width, height; 117 | transition: 118 | transform .5s, 119 | width .5s, 120 | height .5s, 121 | filter .5s; 122 | 123 | transition-timing-function: var(--elevate-bezier); 124 | border-radius: 9999px; 125 | pointer-events: none; 126 | box-shadow: var(--elevate-before); 127 | background-image: var(--a1-gradient); 128 | border: none; 129 | } 130 | -------------------------------------------------------------------------------- /src/components/target/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getSizeKey, nameToId } from '../../helpers' 3 | import Size from '../size' 4 | import css from './target.module.css' 5 | import { TargetApp } from '../../sizes' 6 | import { UCMeta } from '../../types' 7 | import { TabsEnum } from '../tabs' 8 | // @ts-ignore 9 | import butter from 'image-butter' 10 | interface Props extends TargetApp { 11 | ucMeta: UCMeta, 12 | src: string, 13 | mode: TabsEnum 14 | } 15 | 16 | const Target: React.FC = props => { 17 | 18 | React.useEffect(() => { 19 | butter() 20 | }, [props.src]) 21 | 22 | // sort sizes by aspect ratio — taller ones come first 23 | const sortedSizes = [...props.sizes].sort((a, b) => { 24 | const byRatio = (a.width / a.height) - (b.width / b.height) 25 | if (byRatio !== 0) return byRatio 26 | return b.height - a.height 27 | }) 28 | 29 | const targetId = nameToId(props.app) 30 | 31 | return ( 32 |
33 |
34 | 42 |

43 | Pictures for {props.app} 44 |

45 |
46 | 47 |

48 | {props.description} 49 |

50 | 51 |
52 | {sortedSizes.map(size => { 53 | const key = getSizeKey({ 54 | ...size, 55 | app: props.app 56 | }) 57 | 58 | const sizeRest = { 59 | ...size, 60 | description: props.mode === TabsEnum.simple ? size.description : undefined, 61 | positionSrc: props.mode === TabsEnum.simple ? size.positionSrc : undefined, 62 | } 63 | 64 | return ( 65 | 72 | ) 73 | })} 74 |
75 |
76 | ) 77 | } 78 | 79 | export default Target 80 | -------------------------------------------------------------------------------- /src/components/target/target.module.css: -------------------------------------------------------------------------------- 1 | .root + .root { 2 | margin-top: 4rem; 3 | } 4 | 5 | .heading { 6 | margin-top: 0; 7 | margin-bottom: 0; 8 | display: inline; 9 | vertical-align: top; 10 | line-height: 40px; 11 | } 12 | 13 | .image { 14 | display: inline; 15 | margin-right: 1em; 16 | line-height: 1; 17 | filter: var(--elevate-before-filter); 18 | } 19 | 20 | .grid { 21 | display: flex; 22 | flex-wrap: wrap; 23 | margin: -.5rem; 24 | } 25 | 26 | .grid > * { 27 | flex-shrink: 2; 28 | margin: .5rem; 29 | } 30 | 31 | .description { 32 | max-width: 30rem; 33 | } 34 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --t-weaker: #d1d1d1; 3 | --t-weak: #767676; 4 | --t: #37352f; 5 | --t-strong: #1d1c1b; 6 | --t-stronger: #000000; 7 | 8 | --b-weaker: #ffffff; 9 | --b-weak: #f8f8f7; 10 | --b: #f2f1ed; 11 | --b-strong: #e8e7e0; 12 | --b-stronger: #c1c1c1; 13 | 14 | --a1-weaker: #a7e2bf; 15 | --a1-weak: #71d099; 16 | --a1: #3cb46e; 17 | --a1-strong: #37a465; 18 | --a1-stronger: #2a7e4d; 19 | 20 | --a1-gradient: linear-gradient(-45deg, #3fa86b, #42c478); 21 | --a1-gradient-strong: linear-gradient(-45deg, #37a465, #37a465); 22 | 23 | --a1-translucent: #3cb46eaa; 24 | --a1-translucent-weaker: #3cb46e77; 25 | 26 | --a1-c: #ffffff; 27 | 28 | --shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.04); 29 | 30 | --elevate-bezier: cubic-bezier(.17, .67, .24, 1.01); 31 | --elevate-before: 32 | 0 1px 1px rgba(0, 0, 0, 0.05), 33 | 0 2px 2px rgba(0, 0, 0, 0.05), 34 | 0 4px 4px rgba(0, 0, 0, 0.05), 35 | 0 8px 8px rgba(0, 0, 0, 0.05), 36 | 0 16px 16px rgba(0, 0, 0, 0.05); 37 | 38 | --elevate-before-filter: 39 | drop-shadow(0 1px 1px rgba(0, 0, 0, 0.03)) 40 | drop-shadow(0 2px 2px rgba(0, 0, 0, 0.03)) 41 | drop-shadow(0 4px 4px rgba(0, 0, 0, 0.03)) 42 | drop-shadow(0 8px 8px rgba(0, 0, 0, 0.03)) 43 | drop-shadow(0 16px 16px rgba(0, 0, 0, 0.03)); 44 | 45 | --elevate-weak: 46 | 0 1px 1px rgba(0, 0, 0, 0.03), 47 | 0 2px 2px rgba(0, 0, 0, 0.03), 48 | 0 4px 4px rgba(0, 0, 0, 0.03), 49 | 0 8px 8px rgba(0, 0, 0, 0.03), 50 | 0 16px 16px rgba(0, 0, 0, 0.03); 51 | 52 | --separate: 53 | 1px 0 1px 1px var(--b), 54 | 2px 0 2px 2px var(--b), 55 | 4px 0 4px 4px var(--b), 56 | 8px 0 8px 8px var(--b), 57 | 32px 0 32px 32px var(--b), 58 | 64px 0 64px 64px var(--b); 59 | 60 | --elevate-after: 61 | 0 6px 6px rgba(0, 0, 0, 0.05), 62 | 0 7px 7px rgba(0, 0, 0, 0.05), 63 | 0 9px 9px rgba(0, 0, 0, 0.05), 64 | 0 13px 13px rgba(0, 0, 0, 0.05), 65 | 0 21px 21px rgba(0, 0, 0, 0.05); 66 | 67 | --neumorphic: 68 | .5em .5em 1em var(--b-stronger), 69 | -.5em -.5em 10px var(--b-weaker); 70 | 71 | --link-bg: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3Crect x='0' y='.5' width='1' height='.5' fill='rgba(0, 0, 0, .3)'/%3E%3C/svg%3E"); 72 | 73 | --link-bg-focus: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3Crect x='0' y='.5' width='1' height='.5' fill='rgba(237, 6, 12, .3)' /%3E%3C/svg%3E"); 74 | } 75 | 76 | body { 77 | color: var(--t); 78 | background-color: var(--b); 79 | overflow-x: hidden; 80 | } 81 | 82 | /*! normilio v1.0.0 | BSL-1.0 License | https://github.com/mvoloskov/normilio */ 83 | *, 84 | *::before, 85 | *::after { 86 | box-sizing: border-box; 87 | line-height: 1.5; 88 | backface-visibility: hidden; 89 | text-decoration: inherit; 90 | vertical-align: inherit; 91 | } 92 | 93 | :root { 94 | font-variant-ligatures: common-ligatures; 95 | text-rendering: optimizeLegibility; 96 | font-family: 97 | -apple-system, 98 | BlinkMacSystemFont, 99 | "Segoe UI", 100 | Roboto, 101 | Oxygen, 102 | Ubuntu, 103 | Cantarell, 104 | "Fira Sans", 105 | "Droid Sans", 106 | "Helvetica Neue", 107 | sans-serif, 108 | "Apple Color Emoji", 109 | "Segoe UI Emoji", 110 | "Segoe UI Symbol"; 111 | } 112 | 113 | code, 114 | pre { 115 | tab-size: 4; 116 | font-family: monospace; 117 | } 118 | 119 | body { 120 | margin: 0; 121 | } 122 | 123 | img { 124 | display: block; 125 | height: auto; 126 | } 127 | 128 | img, 129 | canvas, 130 | iframe, 131 | video, 132 | svg, 133 | select, 134 | textarea { 135 | max-width: 100%; 136 | } 137 | 138 | [hidden] { 139 | display: none !important; 140 | } 141 | 142 | input { 143 | font-family: inherit; 144 | } 145 | 146 | textarea { 147 | resize: vertical; 148 | } 149 | 150 | [aria-busy="true"] { 151 | cursor: progress; 152 | } 153 | 154 | [disabled], 155 | [aria-disabled="true"] { 156 | cursor: default; 157 | } 158 | 159 | [aria-controls] { 160 | cursor: pointer; 161 | } 162 | 163 | button { 164 | cursor: pointer; 165 | font-family: inherit; 166 | } 167 | 168 | .butter-loading { 169 | filter: contrast(0%) brightness(150%) !important; 170 | } 171 | 172 | .butter-loading.butter-loaded { 173 | filter: none !important; 174 | } 175 | 176 | /* MailChimp Form Embed Code - Slim - 12/15/2015 v10.7 */ 177 | #mc_embed_signup form {display:block; position:relative; text-align:left; max-width: 20rem;} 178 | #mc_embed_signup h2 {font-weight:bold; padding:0; margin:15px 0; font-size:1.4em;} 179 | #mc_embed_signup input {border:1px solid #999; -webkit-appearance:none;} 180 | #mc_embed_signup input[type=checkbox]{-webkit-appearance:checkbox;} 181 | #mc_embed_signup input[type=radio]{-webkit-appearance:radio;} 182 | #mc_embed_signup input:focus {border-color:#333;} 183 | #mc_embed_signup .button {clear:both; background-color: #aaa; border: 0 none; border-radius:4px; letter-spacing:.03em; color: #FFFFFF; cursor: pointer; display: inline-block; font-size:15px; height: 32px; line-height: 32px; margin: 0 5px 10px 0; padding:0; text-align: center; text-decoration: none; vertical-align: top; white-space: nowrap; width: auto; transition: all 0.23s ease-in-out 0s;} 184 | #mc_embed_signup .button:hover {background-color:#777;} 185 | #mc_embed_signup .small-meta {font-size: 11px;} 186 | #mc_embed_signup .nowrap {white-space:nowrap;} 187 | #mc_embed_signup .clear {clear:none; display:inline;} 188 | 189 | #mc_embed_signup label {display:block; font-size:16px; padding-bottom:10px; font-weight:bold;} 190 | #mc_embed_signup input.email {font-size: 15px; display:block; padding:0 0.4em; margin-bottom: 10px; min-height:32px; width:100%; min-width:130px; border-radius: 3px;} 191 | #mc_embed_signup input.button {display:block; width:35%; margin:0 0 10px 0; min-width:90px;} 192 | 193 | #mc_embed_signup div#mce-responses {float:left; top:-1.4em; padding:0em .5em 0em .5em; overflow:hidden; width:90%;margin: 0 5%; clear: both;} 194 | #mc_embed_signup div.response {margin:1em 0; padding:1em .5em .5em 0; font-weight:bold; float:left; top:-1.5em; z-index:1; width:80%;} 195 | #mc_embed_signup #mce-error-response {display:none;} 196 | #mc_embed_signup #mce-success-response {color:#529214; display:none;} 197 | #mc_embed_signup label.error {display:block; float:none; width:auto; margin-left:1.05em; text-align:left; padding:.5em 0;} 198 | 199 | /** 200 | * Author and copyright: Stefan Haack (https://shaack.com) 201 | * Repository: https://github.com/shaack/cookie-consent-js 202 | * License: MIT, see file 'LICENSE' 203 | */ 204 | /* line 4, cookie-consent.scss */ 205 | .cookie-consent-modal { 206 | padding-top: 0; 207 | position: static; 208 | width: auto; 209 | height: auto; 210 | z-index: 1000; 211 | font-family: sans-serif; } 212 | /* line 11, cookie-consent.scss */ 213 | .cookie-consent-modal .modal-content-wrap { 214 | position: fixed; 215 | bottom: 0; 216 | margin: 1rem; } 217 | /* line 15, cookie-consent.scss */ 218 | .cookie-consent-modal .modal-content-wrap.right { 219 | right: 0; } 220 | /* line 18, cookie-consent.scss */ 221 | .cookie-consent-modal .modal-content-wrap.left { 222 | left: 0; } 223 | /* line 21, cookie-consent.scss */ 224 | .cookie-consent-modal .modal-content-wrap .modal-content { 225 | border: 1px solid rgba(0, 0, 0, 0.2); 226 | background-color: #fefefe; 227 | color: #123; 228 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.4); 229 | padding: 0; 230 | max-width: 700px; 231 | border-radius: 0.2rem; } 232 | /* line 31, cookie-consent.scss */ 233 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-header { 234 | padding: 1rem; 235 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); } 236 | /* line 35, cookie-consent.scss */ 237 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-header h3 { 238 | margin: 0; 239 | font-size: 130%; 240 | font-weight: 500; 241 | position: relative; 242 | top: 0.2rem; } 243 | /* line 44, cookie-consent.scss */ 244 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-body { 245 | padding: 1rem 1rem; 246 | line-height: 1.3; } 247 | /* line 48, cookie-consent.scss */ 248 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-body a { 249 | color: #3579F6; } 250 | /* line 51, cookie-consent.scss */ 251 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-body a:hover { 252 | color: #0b5bed; } 253 | /* line 57, cookie-consent.scss */ 254 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer { 255 | padding: 1rem 0.5rem 0.5rem 0.5rem; } 256 | /* line 61, cookie-consent.scss */ 257 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons { 258 | display: flex; 259 | justify-content: flex-end; 260 | flex-wrap: wrap; } 261 | /* line 66, cookie-consent.scss */ 262 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons .btn { 263 | padding: 0.7rem 1.1rem; 264 | font-size: 100%; 265 | cursor: pointer; 266 | border: none; 267 | border-radius: 0.2rem; 268 | margin-left: 0.5rem; 269 | margin-right: 0.5rem; 270 | margin-bottom: 0.5rem; } 271 | /* line 77, cookie-consent.scss */ 272 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons .btn.btn-primary { 273 | background-color: #3579F6; 274 | color: white; } 275 | /* line 81, cookie-consent.scss */ 276 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons .btn.btn-primary:hover { 277 | background-color: #0b5bed; } 278 | /* line 87, cookie-consent.scss */ 279 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons .btn.btn-secondary { 280 | background-color: #89a; 281 | color: white; } 282 | /* line 91, cookie-consent.scss */ 283 | .cookie-consent-modal .modal-content-wrap .modal-content .modal-footer .buttons .btn.btn-secondary:hover { 284 | background-color: #6a8095; } 285 | @media screen and (max-width: 620px) { 286 | /* line 102, cookie-consent.scss */ 287 | .cookie-consent-modal .btn { 288 | width: 100%; } } 289 | /* line 107, cookie-consent.scss */ 290 | .cookie-consent-modal.block-access { 291 | position: fixed; 292 | background-color: rgba(0, 0, 0, 0.5); 293 | padding-top: 20vh; 294 | left: 0; 295 | top: 0; 296 | width: 100%; 297 | height: 100%; 298 | overflow: auto; } 299 | @media screen and (max-width: 620px) { 300 | /* line 107, cookie-consent.scss */ 301 | .cookie-consent-modal.block-access { 302 | padding-top: 0; } } 303 | /* line 119, cookie-consent.scss */ 304 | .cookie-consent-modal.block-access .modal-content-wrap { 305 | position: relative; 306 | margin: 2.5% auto; 307 | bottom: auto; 308 | width: 95%; } 309 | /* line 122, cookie-consent.scss */ 310 | .cookie-consent-modal.block-access .modal-content-wrap .modal-content { 311 | border: none; 312 | margin: 0 auto; } 313 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip' 2 | import { StandaloneSize, SizeWithSrc, SizeWithBlob, UCMeta } from './types' 3 | import { Sizes } from './sizes' 4 | 5 | const ensureEndingWithSlash = (str: string): string => { 6 | const lastSymbol = str[str.length - 1] 7 | if (lastSymbol === '/') return str 8 | return `${str}/` 9 | } 10 | 11 | export const getUrl = (src: string, width: number, height: number, ucMeta: UCMeta): string => { 12 | const srcWithSlash = ensureEndingWithSlash(src) 13 | return ucMeta.compress ? `${srcWithSlash}-/scale_crop/${width}x${height}/smart/-/quality/smart/` : `${srcWithSlash}-/scale_crop/${width}x${height}/smart/` 14 | } 15 | 16 | // needed to prevent adblocks from blocking common ad sizes images 17 | export const getCrookedUrl = (src: string, width: number, height: number, ucMeta: UCMeta) => getUrl(src, width + 1, height + 1, ucMeta) 18 | 19 | export const getSizeKey = (size: StandaloneSize): string => `${size.app} ${size.name} (${size.width}x${size.height})` 20 | 21 | export const downloadSize = (size: SizeWithSrc, ucMeta: UCMeta): Promise => { 22 | const url = getUrl(size.src, size.width, size.height, ucMeta).replace('https://ucarecdn.com/', '') 23 | return fetch(url) 24 | .then(response => response.blob()) 25 | .then(blob => ({ 26 | ...size, 27 | blob, 28 | })) 29 | } 30 | 31 | export const downloadAbstractContent = (href: string, filename: string): void => { 32 | const a = document.createElement('a') 33 | a.href = href 34 | a.setAttribute('download', filename) 35 | a.setAttribute('_target', 'blank') 36 | a.click() 37 | } 38 | 39 | export const downloadFile = (base64content: string, filename: string): void => downloadAbstractContent(`data:application/zip;base64,${base64content}`, filename) 40 | 41 | export const downloadSizes = (sizes: SizeWithSrc[], ucMeta: UCMeta): Promise => 42 | new Promise((resolve, reject) => { 43 | const downloadSizeWithMeta = (meta: UCMeta) => (size: SizeWithSrc) => downloadSize(size, meta) 44 | 45 | Promise.allSettled(sizes.map(downloadSizeWithMeta(ucMeta))) 46 | .then(descs => { 47 | const zip = new JSZip() 48 | descs.forEach(desc => { 49 | if (desc.status !== 'fulfilled') return 50 | const sizeWithBlob = desc.value 51 | const fileName = `${getSizeKey(sizeWithBlob)}.${ucMeta.extension}` 52 | zip.file(fileName, sizeWithBlob.blob) 53 | }) 54 | 55 | zip 56 | .generateAsync({ 57 | type: 'base64', 58 | }) 59 | .then(content => { 60 | resolve() 61 | downloadFile(content, 'pixelhunter-social-media-images.zip') 62 | }) 63 | .catch(reject) 64 | }) 65 | .catch(reject) 66 | }) 67 | 68 | export const mimeToExtension = (mime: string | null): string => { 69 | if (!mime) return 'jpg' 70 | const lowercaseMime = mime.toLowerCase() 71 | if (lowercaseMime === 'image/gif') return 'gif' 72 | if (['image/jpeg', 'image/pjpeg'].includes(lowercaseMime)) return 'jpg' 73 | if (lowercaseMime === 'image/png') return 'png' 74 | if (lowercaseMime === 'image/svg+xml') return 'svg' 75 | if (lowercaseMime === 'image/tiff') return 'tiff' 76 | if (lowercaseMime === 'image/vnd.microsoft.icon') return 'ico' 77 | if (lowercaseMime === 'image/vnd.wap.wbmp') return 'wbmp' 78 | if (lowercaseMime === 'image/webp') return 'webp' 79 | 80 | const mimeTuple = lowercaseMime.split('/') 81 | return mimeTuple[mimeTuple.length - 1] 82 | } 83 | 84 | export const getSimpleModeSizes = (targets: Sizes) => 85 | targets 86 | .map(target => ({ 87 | ...target, 88 | sizes: target.sizes.filter(size => size.simple), 89 | })) 90 | .filter(target => target.sizes.length > 0) 91 | 92 | export const nameToId = (name: string): string => { 93 | return name.replaceAll(' ', '') 94 | } 95 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | import App from './App' 3 | import './global.css' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/sizes.d.ts: -------------------------------------------------------------------------------- 1 | export interface Size { 2 | /** 3 | * @description taget service: short description of this size: Avatar, Cover, etc. Should be unique. 4 | */ 5 | name: string, 6 | 7 | /** 8 | * @description width in pixels 9 | */ 10 | width: number, 11 | 12 | /** 13 | * @description height in pixels 14 | */ 15 | height: number, 16 | 17 | /** 18 | * @description should this target size be featured in "Simple mode" 19 | */ 20 | simple?: boolean, 21 | 22 | /** 23 | * @description a src of the image that explains where this size will be displayed on the corresponding social media. Will be put in an img tag and inserted as-is. 24 | */ 25 | positionSrc?: string, 26 | 27 | description?: string, 28 | } 29 | 30 | export interface TargetApp { 31 | /** 32 | * @description taget service: Facebook, Instagram, etc. Case-sensitive. Should be unique. 33 | */ 34 | app: string, 35 | 36 | /** 37 | * @description some SEO description text about the target service. 38 | */ 39 | description: string, 40 | 41 | /** 42 | * @description a logo image src. Will be put in an img tag and inserted as-is. 43 | */ 44 | logoSrc: string, 45 | 46 | sizes: Size[], 47 | } 48 | 49 | export type Sizes = TargetApp[] 50 | 51 | export default Sizes 52 | -------------------------------------------------------------------------------- /src/sizes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "app": "Facebook", 4 | "description": "Because the size of this social network, Facebook images are mostly utilitarian. Don't expect people to passionately explore them and pin them to their moodboards. However, Facebook supports a wide variety of sizes, so you can utilize them to build a solid brand image.", 5 | "logoSrc": "logos/facebook.svg", 6 | "sizes": [ 7 | { 8 | "name": "Profile picture", 9 | "width": 180, 10 | "height": 180, 11 | "simple": true, 12 | "positionSrc": "positions/facebook-profile-picture.svg", 13 | "description": "Pro Tip: People see this picture everywhere they stumble upon your page. Make sure it's a good one." 14 | }, 15 | { 16 | "name": "Cover photo", 17 | "width": 820, 18 | "height": 312, 19 | "simple": true, 20 | "positionSrc": "positions/facebook-cover-photo.svg", 21 | "description": "Pro Tip: Treat this like a billboard. Changing this picture regularly is usually a good decision. Experiment with images to see what your audience responds to best." 22 | }, 23 | { 24 | "name": "Image post", 25 | "width": 1200, 26 | "height": 630, 27 | "simple": true, 28 | "positionSrc": "positions/facebook-image-post.svg", 29 | "description": "Pro Tip: Because posts with singular images take full width, you may use text as well as highly-detailed images. They will be easily recognizable and noticeable." 30 | }, 31 | { 32 | "name": "Marketplace", 33 | "width": 1200, 34 | "height": 1200 35 | }, 36 | { 37 | "name": "Stories", 38 | "width": 1080, 39 | "height": 1920 40 | }, 41 | { 42 | "name": "Link image", 43 | "width": 1200, 44 | "height": 630 45 | }, 46 | { 47 | "name": "Event image", 48 | "width": 1920, 49 | "height": 1005 50 | }, 51 | { 52 | "name": "Instant articles", 53 | "width": 1200, 54 | "height": 1200 55 | }, 56 | { 57 | "name": "Right column", 58 | "width": 1200, 59 | "height": 1200 60 | }, 61 | { 62 | "name": "Group cover", 63 | "width": 1640, 64 | "height": 856 65 | }, 66 | { 67 | "name": "Highlighted image", 68 | "width": 1200, 69 | "height": 717 70 | }, 71 | { 72 | "name": "Feed", 73 | "width": 1080, 74 | "height": 1350 75 | } 76 | ] 77 | }, 78 | { 79 | "app": "Facebook Ads", 80 | "description": "Remember that your ads will be cut by ad blockers. After that, they will be displayed among many others. Sometimes your ad will be put into a rotator that will switch between your ad and other people's ads several times each minute, so it's better to keep them recognizeable and straightforward.", 81 | "logoSrc": "logos/facebook-ads.svg", 82 | "sizes": [ 83 | { 84 | "name": "Image ad", 85 | "width": 1200, 86 | "height": 628 87 | }, 88 | { 89 | "name": "Story ad", 90 | "width": 1080, 91 | "height": 1920 92 | }, 93 | { 94 | "name": "Audience network", 95 | "width": 1200, 96 | "height": 628 97 | }, 98 | { 99 | "name": "Carousel image", 100 | "width": 1200, 101 | "height": 1200 102 | }, 103 | { 104 | "name": "Messenger image", 105 | "width": 1200, 106 | "height": 628 107 | }, 108 | { 109 | "name": "Audience link", 110 | "width": 398, 111 | "height": 208 112 | } 113 | ] 114 | }, 115 | { 116 | "app": "Google Banner Ads", 117 | "description": "Your ads will be displayed across countless websites that are part of Google Ad Network. Always test your images on different backgrounds to ensure they work in all circumstances.", 118 | "logoSrc": "logos/google.svg", 119 | "sizes": [ 120 | { 121 | "name": "Skyscraper", 122 | "width": 120, 123 | "height": 600 124 | }, 125 | { 126 | "name": "Leaderboard", 127 | "width": 728, 128 | "height": 90 129 | }, 130 | { 131 | "name": "Mobile banner 1", 132 | "width": 300, 133 | "height": 50 134 | }, 135 | { 136 | "name": "Banner", 137 | "width": 468, 138 | "height": 60 139 | }, 140 | { 141 | "name": "Portrait", 142 | "width": 300, 143 | "height": 1050 144 | }, 145 | { 146 | "name": "Panorama", 147 | "width": 980, 148 | "height": 120 149 | }, 150 | { 151 | "name": "Mobile banner 2", 152 | "width": 320, 153 | "height": 100 154 | }, 155 | { 156 | "name": "Billboard", 157 | "width": 970, 158 | "height": 250 159 | }, 160 | { 161 | "name": "Vertical rectangle", 162 | "width": 240, 163 | "height": 400 164 | }, 165 | { 166 | "name": "Half banner", 167 | "width": 234, 168 | "height": 60 169 | }, 170 | { 171 | "name": "Small square", 172 | "width": 200, 173 | "height": 200 174 | }, 175 | { 176 | "name": "Wide skyscraper", 177 | "width": 160, 178 | "height": 600 179 | }, 180 | { 181 | "name": "Large leaderboard", 182 | "width": 970, 183 | "height": 90 184 | }, 185 | { 186 | "name": "Mobile banner 3", 187 | "width": 320, 188 | "height": 50 189 | }, 190 | { 191 | "name": "Top banner", 192 | "width": 930, 193 | "height": 180 194 | }, 195 | { 196 | "name": "Square", 197 | "width": 250, 198 | "height": 250 199 | }, 200 | { 201 | "name": "Medium rectangle", 202 | "width": 300, 203 | "height": 250 204 | }, 205 | { 206 | "name": "Netboard", 207 | "width": 580, 208 | "height": 400 209 | }, 210 | { 211 | "name": "Half-page ad", 212 | "width": 300, 213 | "height": 600 214 | }, 215 | { 216 | "name": "Triple widescreen", 217 | "width": 250, 218 | "height": 360 219 | }, 220 | { 221 | "name": "Inline rectangle", 222 | "width": 300, 223 | "height": 250 224 | }, 225 | { 226 | "name": "Large rectangle", 227 | "width": 336, 228 | "height": 280 229 | } 230 | ] 231 | }, 232 | { 233 | "app": "Instagram", 234 | "description": "It's a part Facebook but it works different than Facebook itself. Today's Instagram is all about color palettes and visual aesthetics. People even design multiple images in a way that they look good together put into grid. Keep your images as visually appealing as possible.", 235 | "logoSrc": "logos/instagram.svg", 236 | "sizes": [ 237 | { 238 | "name": "Profile picture", 239 | "width": 320, 240 | "height": 320, 241 | "simple": true, 242 | "positionSrc": "positions/instagram-profile-picture.svg", 243 | "description": "Pro Tip: Keep your profile picture evergreen updating it as you travel or as the season changes. Try to match the palette and the overall visual aesthetic of your whole profile." 244 | }, 245 | { 246 | "name": "New post", 247 | "width": 1080, 248 | "height": 1350, 249 | "simple": true, 250 | "positionSrc": "positions/instagram-square-post.svg", 251 | "description": "Pro Tip: this is the new Instagram post format. You should probably use it instead of the old one from the now on." 252 | }, 253 | { 254 | "name": "Square post", 255 | "width": 1080, 256 | "height": 1080, 257 | "simple": true, 258 | "positionSrc": "positions/instagram-square-post.svg", 259 | "description": "Pro Tip: It takes the full width, so you can use text and details. They will be seen. Consider not only that but the palette of the whole image as well because it will be shown both large and small." 260 | }, 261 | { 262 | "name": "Stories", 263 | "width": 1080, 264 | "height": 1920, 265 | "simple": true, 266 | "positionSrc": "positions/instagram-stories.svg", 267 | "description": "Pro Tip: Because Instagram supports additional interactive Stories features such as stickers and polls, you can design your Stories cover with that in mind, carefully placing blank areas where interactive elements would be." 268 | }, 269 | { 270 | "name": "Ad square", 271 | "width": 1080, 272 | "height": 1080 273 | }, 274 | { 275 | "name": "Ad portrait", 276 | "width": 1080, 277 | "height": 1350 278 | }, 279 | { 280 | "name": "Reels", 281 | "width": 1080, 282 | "height": 1920 283 | }, 284 | { 285 | "name": "Ad landscape", 286 | "width": 1080, 287 | "height": 566 288 | }, 289 | { 290 | "name": "IGTV cover", 291 | "width": 420, 292 | "height": 654 293 | }, 294 | { 295 | "name": "Landscape post", 296 | "width": 1080, 297 | "height": 566 298 | }, 299 | { 300 | "name": "Portrait post", 301 | "width": 1080, 302 | "height": 1350 303 | }, 304 | { 305 | "name": "Photo thumbnails", 306 | "width": 161, 307 | "height": 161 308 | }, 309 | { 310 | "name": "Feed", 311 | "width": 1080, 312 | "height": 1350 313 | } 314 | ] 315 | }, 316 | { 317 | "app": "Twitter", 318 | "description": "Even though supported, pictures are not the main part of Twitter feeds — text is the king. However, Twitter provides every way for you to design your profile to make it sleek and appealing.", 319 | "logoSrc": "logos/twitter.svg", 320 | "sizes": [ 321 | { 322 | "name": "Profile photo", 323 | "width": 400, 324 | "height": 400, 325 | "simple": true, 326 | "positionSrc": "positions/twitter-profile-photo.svg", 327 | "description": "Pro Tip: Create the whole new profile experience by matching your profile photo with your Header picture." 328 | }, 329 | { 330 | "name": "Header", 331 | "width": 1500, 332 | "height": 500, 333 | "simple": true, 334 | "positionSrc": "positions/twitter-header.svg", 335 | "description": "Pro Tip: Because it will be displayed really wide, you can freely experiment with the layout. However, sometimes this image is cropped vertically, so don't put anything important to the very bottom of this image." 336 | }, 337 | { 338 | "name": "Post image", 339 | "width": 1200, 340 | "height": 675, 341 | "simple": true, 342 | "positionSrc": "positions/twitter-post-image.svg", 343 | "description": "Pro Tip: It's slightly taller than an Open Graph image, but you may use Open Graph if you're designing a picture for the Twitter Card." 344 | }, 345 | { 346 | "name": "Conversation card", 347 | "width": 800, 348 | "height": 418 349 | }, 350 | { 351 | "name": "Fleets images", 352 | "width": 1080, 353 | "height": 1920 354 | }, 355 | { 356 | "name": "Video thumbnail", 357 | "width": 640, 358 | "height": 360 359 | }, 360 | { 361 | "name": "Website card ad", 362 | "width": 800, 363 | "height": 800 364 | }, 365 | { 366 | "name": "Card image (min)", 367 | "width": 120, 368 | "height": 120 369 | }, 370 | { 371 | "name": "Direct message", 372 | "width": 800, 373 | "height": 418 374 | }, 375 | { 376 | "name": "App card ad", 377 | "width": 800, 378 | "height": 800 379 | }, 380 | { 381 | "name": "Carousels", 382 | "width": 800, 383 | "height": 800 384 | } 385 | ] 386 | }, 387 | { 388 | "app": "YouTube", 389 | "description": "On the world's largest hosting for videos, pictures are still very important. On YouTube, they are the instrument of attracting new audience, less so of keeping the existing one entertained. As the platform changes and clickbait falls out of grace, it's better to keep video thumbnails informative and truthful.", 390 | "logoSrc": "logos/youtube.svg", 391 | "sizes": [ 392 | { 393 | "name": "Profile picture", 394 | "width": 800, 395 | "height": 800, 396 | "simple": true, 397 | "positionSrc": "positions/youtube-profile-picture.svg", 398 | "description": "Pro Tip: You can color-match your Profile picture with your Channel art to ensure premium experience. However, it's best not to leave your profile picture blank because it would also be displayed on its own." 399 | }, 400 | { 401 | "name": "Channel art", 402 | "width": 2560, 403 | "height": 1440, 404 | "simple": true, 405 | "positionSrc": "positions/youtube-channel-art.svg", 406 | "description": "Pro Tip: Unlike in Twitter, on YouTube the channel art is not cropped vertically, so you can freely experiment with what content you put there." 407 | }, 408 | { 409 | "name": "Thumbnail", 410 | "width": 1280, 411 | "height": 720, 412 | "simple": true, 413 | "positionSrc": "positions/youtube-thumbnail.svg", 414 | "description": "Pro Tip: Clickbait is a thing of the past. Nowadays it may damage how your audience perceives your channel. It's better to refrain from blank, filler thumbnails and instead choose the one that would be straight to the point." 415 | }, 416 | { 417 | "name": "Display ads", 418 | "width": 300, 419 | "height": 250 420 | }, 421 | { 422 | "name": "Companion banner", 423 | "width": 300, 424 | "height": 60 425 | }, 426 | { 427 | "name": "Overlay ads", 428 | "width": 480, 429 | "height": 60 430 | }, 431 | { 432 | "name": "Channel Art TV", 433 | "width": 2560, 434 | "height": 1440 435 | }, 436 | { 437 | "name": "Channel Art PC", 438 | "width": 2560, 439 | "height": 423 440 | }, 441 | { 442 | "name": "Channel Art tablet", 443 | "width": 1855, 444 | "height": 423 445 | }, 446 | { 447 | "name": "Channel Art mobile", 448 | "width": 1546, 449 | "height": 423 450 | } 451 | ] 452 | }, 453 | { 454 | "app": "TikTok", 455 | "description": "This fast-paced social media was so influential that even YouTube made every attempt to catch on. Because of the speed of content consumption and its variety it's pretty much impossible to predict trends on TikTok, so the key is building the loyal audience that watches what you do because of who you are.", 456 | "logoSrc": "logos/tiktok.svg", 457 | "sizes": [ 458 | { 459 | "name": "Profile photo", 460 | "width": 200, 461 | "height": 200, 462 | "simple": true, 463 | "description": "Pro Tip: It's really small. Be careful with the details because they may not be recognizeable." 464 | }, 465 | { 466 | "name": "Vertical video ad", 467 | "width": 1080, 468 | "height": 1920, 469 | "simple": true, 470 | "description": "Pro Tip: Keep meaningful content away from edges and corners of the picture because TikTok UI features may obscure it." 471 | }, 472 | { 473 | "name": "In-feed ad", 474 | "width": 1080, 475 | "height": 1080 476 | }, 477 | { 478 | "name": "Square video", 479 | "width": 1080, 480 | "height": 1080 481 | }, 482 | { 483 | "name": "Horizontal video", 484 | "width": 1920, 485 | "height": 1080 486 | } 487 | ] 488 | }, 489 | { 490 | "app": "LinkedIn", 491 | "description": "The number one professional social network even consider inappropriate some types of communication that are perfectly okay elsewhere. Because of its flexibility, LinkedIn allows you to customize your profile in every way you need to build your brand image while keeping it strictly professional.", 492 | "logoSrc": "logos/linkedin.svg", 493 | "sizes": [ 494 | { 495 | "name": "Logo", 496 | "width": 300, 497 | "height": 300 498 | }, 499 | { 500 | "name": "Background photo", 501 | "width": 1584, 502 | "height": 396 503 | }, 504 | { 505 | "name": "Link post", 506 | "width": 1200, 507 | "height": 627 508 | }, 509 | { 510 | "name": "Stories", 511 | "width": 1080, 512 | "height": 1920 513 | }, 514 | { 515 | "name": "Life tab image", 516 | "width": 1128, 517 | "height": 376 518 | }, 519 | { 520 | "name": "Profile picture", 521 | "width": 400, 522 | "height": 400 523 | }, 524 | { 525 | "name": "Cover photo", 526 | "width": 1128, 527 | "height": 191 528 | }, 529 | { 530 | "name": "Blog post link", 531 | "width": 1200, 532 | "height": 627 533 | }, 534 | { 535 | "name": "Square post", 536 | "width": 1200, 537 | "height": 1200 538 | }, 539 | { 540 | "name": "Dynamic ad", 541 | "width": 100, 542 | "height": 100 543 | }, 544 | { 545 | "name": "Sponsored content", 546 | "width": 1200, 547 | "height": 627 548 | }, 549 | { 550 | "name": "Business banner", 551 | "width": 646, 552 | "height": 220 553 | }, 554 | { 555 | "name": "Portrait post", 556 | "width": 1080, 557 | "height": 1350 558 | } 559 | ] 560 | }, 561 | { 562 | "app": "Pinterest", 563 | "description": "Visual aesthetics is importnat on any social media, but on Pinterest it's the only thing that matters. Even more than Instagram, Pinterest is focused entirely around visuals. This is the place where people build moodboards and design portfolios.", 564 | "logoSrc": "logos/pinterest.svg", 565 | "sizes": [ 566 | { 567 | "name": "Portrait carousel", 568 | "width": 1000, 569 | "height": 1500 570 | }, 571 | { 572 | "name": "Story pins", 573 | "width": 1080, 574 | "height": 1920 575 | }, 576 | { 577 | "name": "Profile photo", 578 | "width": 165, 579 | "height": 165 580 | }, 581 | { 582 | "name": "Vertical pin", 583 | "width": 1000, 584 | "height": 1500 585 | }, 586 | { 587 | "name": "Square carousel", 588 | "width": 1000, 589 | "height": 1000 590 | }, 591 | { 592 | "name": "Board display", 593 | "width": 222, 594 | "height": 150 595 | } 596 | ] 597 | }, 598 | { 599 | "app": "Snapchat", 600 | "description": "Being slower than expected to grow on international markets, Snapchat has the most of its presence in the U.S. According to Insider and contrary to a popular belief, Snapchat user base isn't as young as some might think.", 601 | "logoSrc": "logos/snapchat.svg", 602 | "sizes": [ 603 | { 604 | "name": "Image share", 605 | "width": 1080, 606 | "height": 1920 607 | } 608 | ] 609 | }, 610 | { 611 | "app": "Open Graph", 612 | "description": "Open Graph is your one stop shop for making your shared links beautiful whenever they're posted. Basically any social media, messenger and modern note-taking app supports it. If you have to choose just one image of one size, Open Graph is the way to go.", 613 | "logoSrc": "logos/open-graph.svg", 614 | "sizes": [ 615 | { 616 | "name": "Universal", 617 | "width": 1200, 618 | "height": 630, 619 | "simple": true 620 | } 621 | ] 622 | }, 623 | { 624 | "app": "Email", 625 | "description": "Fine-tuned image sizes for different mail clients including GMail, Outlook, Yahoo Mail, Apple Mail App, iCloud Mail and others.", 626 | "logoSrc": "logos/email.svg", 627 | "sizes": [ 628 | { 629 | "name": "Blog image", 630 | "width": 750, 631 | "height": 750 632 | }, 633 | { 634 | "name": "Blog featured", 635 | "width": 1200, 636 | "height": 600 637 | }, 638 | { 639 | "name": "Header image", 640 | "width": 600, 641 | "height": 200 642 | } 643 | ] 644 | } 645 | ] 646 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface AbstractSize { 2 | name: string, 3 | width: number, 4 | height: number, 5 | } 6 | 7 | export interface StandaloneSize extends AbstractSize { 8 | app: string, 9 | } 10 | 11 | export interface SizeWithSrc extends StandaloneSize { 12 | src: string, 13 | } 14 | 15 | export interface SizeWithBlob extends StandaloneSize { 16 | blob: Blob, 17 | } 18 | 19 | export interface UCMeta { 20 | compress: boolean, 21 | extension: string, 22 | } 23 | -------------------------------------------------------------------------------- /src/vendors/cookie-consent-js.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Author and copyright: Stefan Haack (https://shaack.com) 3 | * Repository: https://github.com/shaack/cookie-consent-js 4 | * License: MIT, see file 'LICENSE' 5 | */ 6 | 7 | export default function CookieConsent(props) { 8 | 9 | var self = this 10 | this.props = { 11 | buttonPrimaryClass: "btn btn-primary", // the "accept all" buttons class, only used for styling 12 | buttonSecondaryClass: "btn btn-secondary", // the "accept necessary" buttons class, only used for styling 13 | privacyPolicyUrl: "privacy-policy.html", 14 | autoShowModal: true, // disable autoShowModal on the privacy policy page, to make that page readable 15 | lang: navigator.language, // the language, in which the modal is shown 16 | blockAccess: false, // set "true" to block the access to the website before choosing a cookie configuration 17 | position: "right", // position ("left" or "right"), if blockAccess is false 18 | postSelectionCallback: undefined, // callback, after the user has made his selection 19 | content: { // the content in all needed languages 20 | de: { 21 | title: "Cookie-Einstellungen", 22 | body: "Wir nutzen Cookies, um Inhalte zu personalisieren und die Zugriffe auf unsere Website zu analysieren. " + 23 | "Sie können wählen, ob Sie nur für die Funktion der Website notwendige Cookies akzeptieren oder auch " + 24 | "Tracking-Cookies zulassen möchten. Weitere Informationen finden Sie in unserer --privacy-policy--.", 25 | privacyPolicy: "Datenschutzerklärung", 26 | buttonAcceptAll: "Alle Cookies akzeptieren", 27 | buttonAcceptTechnical: "Nur technisch notwendige Cookies akzeptieren" 28 | }, 29 | en: { 30 | title: "Cookie settings", 31 | body: "We use cookies to personalize content and analyze access to our website. " + 32 | "You can choose whether you only accept cookies that are necessary for the functioning of the website " + 33 | "or whether you also want to allow tracking cookies. For more information, please refer to our --privacy-policy--.", 34 | privacyPolicy: "privacy policy", 35 | buttonAcceptAll: "Accept all cookies", 36 | buttonAcceptTechnical: "Decline" 37 | } 38 | }, 39 | cookieName: "cookie-consent-tracking-allowed", // the name of the cookie, the cookie is `true` if tracking was accepted 40 | modalId: "cookieConsentModal" // the id of the modal dialog element 41 | } 42 | for (var property in props) { 43 | // noinspection JSUnfilteredForInLoop 44 | this.props[property] = props[property] 45 | } 46 | this.lang = this.props.lang 47 | if (this.lang.indexOf("-") !== -1) { 48 | this.lang = this.lang.split("-")[0] 49 | } 50 | if (this.props.content[this.lang] === undefined) { 51 | this.lang = "en" // fallback 52 | } 53 | var _t = this.props.content[this.lang] 54 | var linkPrivacyPolicy = '' + _t.privacyPolicy + '' 55 | var modalClass = "cookie-consent-modal" 56 | if (this.props.blockAccess) { 57 | modalClass += " block-access" 58 | } 59 | this.modalContent = '
' + 60 | '' 66 | this.modalContent = this.modalContent.replace(/--header--/, "

" + _t.title + "

") 67 | this.modalContent = this.modalContent.replace(/--body--/, 68 | _t.body.replace(/--privacy-policy--/, linkPrivacyPolicy) 69 | ) 70 | this.modalContent = this.modalContent.replace(/--footer--/, 71 | "
" + 72 | "" + 73 | "" + 74 | "
" 75 | ) 76 | 77 | this.setCookie = function (name, value, days) { 78 | var expires = "" 79 | if (days) { 80 | var date = new Date() 81 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)) 82 | expires = "; expires=" + date.toUTCString() 83 | } 84 | document.cookie = name + "=" + (value || "") + expires + "; Path=/; SameSite=Strict;" 85 | } 86 | 87 | this.getCookie = function(name) { 88 | var nameEQ = name + "=" 89 | var ca = document.cookie.split(';') 90 | for (var i = 0; i < ca.length; i++) { 91 | var c = ca[i] 92 | while (c.charAt(0) === ' ') { 93 | c = c.substring(1, c.length) 94 | } 95 | if (c.indexOf(nameEQ) === 0) { 96 | return c.substring(nameEQ.length, c.length) 97 | } 98 | } 99 | return undefined 100 | } 101 | 102 | this.removeCookie = function (name) { 103 | document.cookie = name + '=; Path=/; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:01 GMT;' 104 | } 105 | 106 | this.documentReady = function (fn) { 107 | if (document.readyState !== 'loading') { 108 | fn() 109 | } else { 110 | document.addEventListener('DOMContentLoaded', fn) 111 | } 112 | } 113 | 114 | this.hideDialog = function () { 115 | this.modal.style.display = "none" 116 | } 117 | 118 | this.showDialog = function () { 119 | this.documentReady(function () { 120 | this.modal = document.getElementById(self.props.modalId) 121 | if (!this.modal) { 122 | this.modal = document.createElement("div") 123 | this.modal.id = self.props.modalId 124 | this.modal.innerHTML = self.modalContent 125 | document.body.append(this.modal) 126 | this.modal.querySelector(".btn-accept-necessary").addEventListener("click", function () { 127 | this.setCookie(self.props.cookieName, "false", 365) 128 | this.hideDialog() 129 | if (self.props.postSelectionCallback) { 130 | self.props.postSelectionCallback(false) 131 | } 132 | }.bind(this)) 133 | this.modal.querySelector(".btn-accept-all").addEventListener("click", function () { 134 | this.setCookie(self.props.cookieName, "true", 365) 135 | this.hideDialog() 136 | if (self.props.postSelectionCallback) { 137 | self.props.postSelectionCallback(true) 138 | } 139 | }.bind(this)) 140 | } else { 141 | this.modal.style.display = "block" 142 | } 143 | }.bind(this)) 144 | } 145 | 146 | if (this.getCookie(this.props.cookieName) === undefined && this.props.autoShowModal) { 147 | this.showDialog() 148 | } 149 | 150 | // API 151 | this.reset = function () { 152 | this.removeCookie(this.props.cookieName) 153 | this.showDialog() 154 | } 155 | 156 | this.trackingAllowed = function () { 157 | return this.getCookie(this.props.cookieName) === "true" 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/vendors/image-zoom.d.ts: -------------------------------------------------------------------------------- 1 | import 'focus-options-polyfill'; 2 | export interface ImageZoomOpts { 3 | closeText?: string; 4 | isControlled?: boolean; 5 | modalText?: string; 6 | onZoomChange?: (isZoomed: boolean) => void; 7 | openText?: string; 8 | overlayBgColor?: string; 9 | overlayOpacity?: number; 10 | transitionDuration?: number; 11 | zoomMargin?: number; 12 | zoomZindex?: number; 13 | } 14 | export interface ImageZoomUpdateOpts extends ImageZoomOpts { 15 | isZoomed?: boolean; 16 | } 17 | interface Update { 18 | (opts?: ImageZoomUpdateOpts): void; 19 | } 20 | export interface ImageZoomReturnType { 21 | cleanup: () => void; 22 | update: Update; 23 | } 24 | declare const ImageZoom: ({ closeText, isControlled, modalText, onZoomChange, openText, overlayBgColor, overlayOpacity, transitionDuration: _transitionDuration, zoomMargin, zoomZindex, }: ImageZoomOpts | undefined, targetEl: HTMLElement) => ImageZoomReturnType; 25 | export default ImageZoom; 26 | -------------------------------------------------------------------------------- /src/vendors/image-zoom.js: -------------------------------------------------------------------------------- 1 | import 'focus-options-polyfill'; 2 | import { focus, addEventListener, getBoundingClientRect, getScaleToWindowMax, getScaleToWindow, createElement, setAttribute, getComputedStyle, setStyleProperty, removeEventListener, removeChild, getParentNode, appendChild, getStyleProperty, getWindowInnerWidth, getWindowInnerHeight, blur, cloneElement, removeAttribute, forEachSibling, getAttribute } from '@rpearce/ts-dom-fns'; 3 | 4 | var State; 5 | (function (State) { 6 | State["LOADED"] = "LOADED"; 7 | State["UNLOADED"] = "UNLOADED"; 8 | State["UNLOADING"] = "UNLOADING"; 9 | })(State || (State = {})); 10 | var LOADED = State.LOADED, UNLOADED = State.UNLOADED, UNLOADING = State.UNLOADING; 11 | var focusPreventScroll = focus.bind(null, { preventScroll: true }); 12 | var ABSOLUTE = 'absolute'; 13 | var ARIA_HIDDEN = 'aria-hidden'; 14 | var ARIA_LABEL = 'aria-label'; 15 | var ARIA_MODAL = 'aria-modal'; 16 | var BG_COLOR_CSS = 'background-color'; 17 | var BLOCK = 'block'; 18 | var BUTTON = 'button'; 19 | var CLICK = 'click'; 20 | var CURSOR = 'cursor'; 21 | var DATA_RMIZ_OVERLAY = 'data-rmiz-overlay'; 22 | var DATA_RMIZ_ZOOMED = 'data-rmiz-zoomed'; 23 | var DIALOG = 'dialog'; 24 | var DISPLAY = 'display'; 25 | var DIV = 'div'; 26 | var FOCUS = 'focus'; 27 | var HEIGHT = 'height'; 28 | var HIDDEN = 'hidden'; 29 | var HUNDRED_PCT = '100%'; 30 | var ID = 'id'; 31 | var KEYDOWN = 'keydown'; 32 | var LEFT = 'left'; 33 | var LOAD = 'load'; 34 | var MARGIN = 'margin'; 35 | var MARGIN_LEFT_JS = MARGIN + "Left"; 36 | var MARGIN_TOP_JS = MARGIN + "Top"; 37 | var MAX_HEIGHT = 'maxHeight'; 38 | var MAX_WIDTH = 'maxWidth'; 39 | var NONE = 'none'; 40 | var OPACITY = 'opacity'; 41 | var POSITION = 'position'; 42 | var RESIZE = 'resize'; 43 | var ROLE = 'role'; 44 | var SCROLL = 'scroll'; 45 | var STYLE = 'style'; 46 | var TABINDEX = 'tabindex'; 47 | var TOP = 'top'; 48 | var TRANSFORM = 'transform'; 49 | var TRANSITION = 'transition'; 50 | var TRANSITIONEND = 'transitionend'; 51 | var TRUE_STR = 'true'; 52 | var TYPE = 'type'; 53 | var VISIBILITY = 'visibility'; 54 | var WIDTH = 'width'; 55 | var ZERO = '0'; 56 | var Z_INDEX_CSS = 'z-index'; 57 | var ImageZoom = function (_a, targetEl) { 58 | 59 | var _b = _a === void 0 ? {} : _a, _c = _b.closeText, closeText = _c === void 0 ? 'Unzoom image' : _c, _d = _b.isControlled, isControlled = _d === void 0 ? false : _d, _e = _b.modalText, modalText = _e === void 0 ? 'Zoomed item' : _e, onZoomChange = _b.onZoomChange, _g = _b.overlayBgColor, overlayBgColor = _g === void 0 ? '#fff' : _g, _h = _b.overlayOpacity, overlayOpacity = _h === void 0 ? 1 : _h, _j = _b.transitionDuration, _transitionDuration = _j === void 0 ? 300 : _j, _k = _b.zoomMargin, zoomMargin = _k === void 0 ? 0 : _k, _l = _b.zoomZindex, zoomZindex = _l === void 0 ? 2147483647 : _l; 60 | var isImgEl = targetEl.tagName === 'IMG'; 61 | var isSvgSrc = isImgEl && SVG_REGEX.test(targetEl.currentSrc); 62 | var isImg = !isSvgSrc && isImgEl; 63 | var documentBody = document.body; 64 | var scrollableEl = window; 65 | var ariaHiddenSiblings = []; 66 | var boundaryDivFirst; 67 | var boundaryDivLast; 68 | var closeBtnEl; 69 | var modalEl; 70 | var motionPref; 71 | var openBtnEl; 72 | var overlayEl; 73 | var state = UNLOADED; 74 | var transitionDuration = _transitionDuration; 75 | var zoomableEl; 76 | var init = function () { 77 | addEventListener(RESIZE, handleResize, window); 78 | initMotionPref(); 79 | if (isImgEl && !targetEl.complete) { 80 | addEventListener(LOAD, handleLoad, targetEl); 81 | } 82 | else { 83 | handleLoad(); 84 | } 85 | }; 86 | // START TARGET MUTATION OBSERVER 87 | var bodyObserver; 88 | var oldTargetEl = targetEl.cloneNode(true); 89 | var initMutationObservers = function () { 90 | var opts = { 91 | attributes: true, 92 | characterData: true, 93 | childList: true, 94 | subtree: true, 95 | }; 96 | var bodyCb = function () { 97 | if (targetEl) { 98 | if (state === UNLOADED && !oldTargetEl.isEqualNode(targetEl)) { 99 | reset(); 100 | oldTargetEl = targetEl.cloneNode(true); 101 | } 102 | } 103 | }; 104 | bodyObserver = new MutationObserver(bodyCb); 105 | bodyObserver.observe(documentBody, opts); 106 | }; 107 | var cleanupMutationObservers = function () { 108 | bodyObserver === null || bodyObserver === void 0 ? void 0 : bodyObserver.disconnect(); 109 | bodyObserver = undefined; 110 | }; 111 | // END TARGET MUTATION OBSERVER 112 | // START MOTION PREFS 113 | var initMotionPref = function () { 114 | motionPref = window.matchMedia('(prefers-reduced-motion:reduce)'); 115 | motionPref.addListener(handleMotionPref); // NOT addEventListener because compatibility 116 | }; 117 | var handleMotionPref = function () { 118 | transitionDuration = 0; 119 | }; 120 | var cleanupMotionPref = function () { 121 | motionPref === null || motionPref === void 0 ? void 0 : motionPref.removeListener(handleMotionPref); // NOT removeEventListener because compatibility 122 | motionPref = undefined; 123 | }; 124 | // END MOTION PREFS 125 | var handleLoad = function () { 126 | if (!targetEl || state !== UNLOADED) 127 | return; 128 | // create openBtnEl 129 | openBtnEl = createElement(DIV); 130 | setAttribute('aria-hidden', 'true', openBtnEl); 131 | setAttribute('tabindex', '-1', openBtnEl); 132 | setAttribute(STYLE, styleZoomBtnIn, openBtnEl); 133 | setAttribute('data-rmiz-trigger', 'true', openBtnEl) 134 | // setAttribute(TYPE, BUTTON, openBtnEl); 135 | adjustOpenBtnEl(); 136 | addEventListener(CLICK, handleOpenBtnClick, openBtnEl); 137 | // insert openBtnEl after targetEl 138 | targetEl.insertAdjacentElement('afterend', openBtnEl); 139 | 140 | initMutationObservers(); 141 | }; 142 | var reset = function () { 143 | cleanup(); 144 | init(); 145 | }; 146 | var adjustOpenBtnEl = function () { 147 | if (!openBtnEl) 148 | return; 149 | var _a = getBoundingClientRect(targetEl), height = _a.height, width = _a.width; 150 | var style = getComputedStyle(targetEl); 151 | var type = style[DISPLAY]; 152 | var marginLeft = parseFloat(style[MARGIN_LEFT_JS]); // eslint-disable-line @typescript-eslint/no-explicit-any 153 | var marginTop = parseFloat(style[MARGIN_TOP_JS]); // eslint-disable-line @typescript-eslint/no-explicit-any 154 | setStyleProperty(WIDTH, width + "px", openBtnEl); 155 | setStyleProperty(HEIGHT, height + "px", openBtnEl); 156 | setStyleProperty(MARGIN_LEFT_JS, marginLeft + "px", openBtnEl); 157 | if (type === BLOCK || 158 | type === 'flex' || 159 | type === 'grid' || 160 | type === 'table') { 161 | setStyleProperty(MARGIN_TOP_JS, "-" + (marginTop + height) + "px", openBtnEl); 162 | } 163 | else { 164 | setStyleProperty(MARGIN_LEFT_JS, marginLeft - width + "px", openBtnEl); 165 | } 166 | }; 167 | var update = function (opts) { 168 | if (opts === void 0) { opts = {}; } 169 | if (opts.closeText) 170 | closeText = opts.closeText; 171 | if (opts.modalText) 172 | modalText = opts.modalText; 173 | if (opts.openText) 174 | if (opts.overlayBgColor) 175 | overlayBgColor = opts.overlayBgColor; 176 | if (opts.overlayOpacity) 177 | overlayOpacity = opts.overlayOpacity; 178 | if (opts.transitionDuration) 179 | transitionDuration = opts.transitionDuration; 180 | if (opts.zoomMargin) 181 | zoomMargin = opts.zoomMargin; 182 | if (opts.zoomZindex) 183 | zoomZindex = opts.zoomZindex; 184 | setZoomImgStyle(false); 185 | if (state === UNLOADED && opts.isZoomed) { 186 | zoom(); 187 | } 188 | else if (state === LOADED && opts.isZoomed === false) { 189 | unzoom(); 190 | } 191 | }; 192 | // START CLEANUP 193 | var cleanup = function () { 194 | cleanupZoom(); 195 | cleanupMutationObservers(); 196 | cleanupTargetLoad(); 197 | cleanupDOMMutations(); 198 | cleanupMotionPref(); 199 | removeEventListener(RESIZE, handleResize, window); 200 | }; 201 | var cleanupTargetLoad = function () { 202 | if (isImg && targetEl) { 203 | removeEventListener(LOAD, handleLoad, targetEl); 204 | } 205 | }; 206 | var cleanupDOMMutations = function () { 207 | if (openBtnEl) { 208 | removeEventListener(CLICK, handleOpenBtnClick, openBtnEl); 209 | removeChild(openBtnEl, getParentNode(openBtnEl)); 210 | } 211 | openBtnEl = undefined; 212 | }; 213 | var cleanupZoom = function () { 214 | removeEventListener(SCROLL, handleScroll, scrollableEl); 215 | removeEventListener(KEYDOWN, handleDocumentKeyDown, document); 216 | if (zoomableEl) { 217 | removeEventListener(LOAD, handleZoomImgLoad, zoomableEl); 218 | removeEventListener(TRANSITIONEND, handleUnzoomTransitionEnd, zoomableEl); 219 | removeEventListener(TRANSITIONEND, handleZoomTransitionEnd, zoomableEl); 220 | } 221 | if (closeBtnEl) { 222 | removeEventListener(CLICK, handleCloseBtnClick, closeBtnEl); 223 | } 224 | if (boundaryDivFirst) { 225 | removeEventListener(FOCUS, handleFocusBoundaryDiv, boundaryDivFirst); 226 | } 227 | if (boundaryDivLast) { 228 | removeEventListener(FOCUS, handleFocusBoundaryDiv, boundaryDivLast); 229 | } 230 | if (modalEl) { 231 | removeEventListener(CLICK, handleModalClick, modalEl); 232 | removeChild(modalEl, documentBody); 233 | } 234 | zoomableEl = undefined; 235 | closeBtnEl = undefined; 236 | boundaryDivFirst = undefined; 237 | boundaryDivLast = undefined; 238 | overlayEl = undefined; 239 | modalEl = undefined; 240 | }; 241 | // END CLEANUP 242 | var handleOpenBtnClick = function () { 243 | if (onZoomChange) { 244 | onZoomChange(true); 245 | } 246 | if (!isControlled) { 247 | zoom(); 248 | } 249 | }; 250 | var handleCloseBtnClick = function () { 251 | if (onZoomChange) { 252 | onZoomChange(false); 253 | } 254 | if (!isControlled) { 255 | unzoom(); 256 | } 257 | }; 258 | var handleFocusBoundaryDiv = function () { 259 | focusPreventScroll(closeBtnEl); 260 | }; 261 | var handleResize = function () { 262 | if (state === LOADED) { 263 | setZoomImgStyle(true); 264 | } 265 | else { 266 | reset(); 267 | } 268 | }; 269 | var handleZoomTransitionEnd = function () { 270 | focusPreventScroll(closeBtnEl); 271 | }; 272 | var handleZoomImgLoad = function () { 273 | if (!zoomableEl) 274 | return; 275 | modalEl = createModal(); 276 | if (!modalEl) 277 | return; 278 | appendChild(modalEl, documentBody); 279 | addEventListener(KEYDOWN, handleDocumentKeyDown, document); 280 | addEventListener(SCROLL, handleScroll, scrollableEl); 281 | if (targetEl) { 282 | setStyleProperty(VISIBILITY, HIDDEN, targetEl); 283 | } 284 | if (zoomableEl) { 285 | addEventListener(TRANSITIONEND, handleZoomTransitionEnd, zoomableEl); 286 | } 287 | state = LOADED; 288 | setZoomImgStyle(false); 289 | ariaHideOtherContent(); 290 | if (overlayEl) { 291 | setAttribute(STYLE, stylePosAbsolute + 292 | styleAllDirsZero + 293 | (BG_COLOR_CSS + ":" + overlayBgColor + ";") + 294 | (TRANSITION + ":" + OPACITY + " " + transitionDuration + "ms " + styleTransitionTimingFn + ";") + 295 | (OPACITY + ":0;"), overlayEl); 296 | setStyleProperty(OPACITY, "" + overlayOpacity, overlayEl); 297 | } 298 | }; 299 | var handleUnzoomTransitionEnd = function () { 300 | if (targetEl) { 301 | setStyleProperty(VISIBILITY, '', targetEl); 302 | } 303 | state = UNLOADED; 304 | setZoomImgStyle(true); 305 | cleanupZoom(); 306 | focusPreventScroll(openBtnEl); 307 | }; 308 | var handleModalClick = function () { 309 | if (onZoomChange) { 310 | onZoomChange(false); 311 | } 312 | if (!isControlled) { 313 | unzoom(); 314 | } 315 | }; 316 | var handleScroll = function () { 317 | if (state === LOADED) { 318 | if (onZoomChange) { 319 | onZoomChange(false); 320 | } 321 | if (!isControlled) { 322 | unzoom(); 323 | } 324 | } 325 | else if (state === UNLOADING) { 326 | setZoomImgStyle(false); 327 | } 328 | }; 329 | var handleDocumentKeyDown = function (e) { 330 | if (isEscapeKey(e)) { 331 | e.stopPropagation(); 332 | if (onZoomChange) { 333 | onZoomChange(false); 334 | } 335 | if (!isControlled) { 336 | unzoom(); 337 | } 338 | } 339 | }; 340 | var setZoomImgStyle = function (instant) { 341 | if (!targetEl || !zoomableEl) 342 | return; 343 | var td = instant ? 0 : transitionDuration; 344 | var _a = targetEl.getBoundingClientRect(), height = _a.height, left = _a.left, top = _a.top, width = _a.width; 345 | var originalTransform = getStyleProperty(TRANSFORM, targetEl); 346 | var transform; 347 | if (state !== LOADED) { 348 | transform = 'scale(1) translate(0,0)' + (originalTransform ? " " + originalTransform : ''); 349 | } 350 | else { 351 | var scale = getScaleToWindow(width, height, zoomMargin); 352 | if (isImg) { 353 | var _b = targetEl, naturalHeight = _b.naturalHeight, naturalWidth = _b.naturalWidth; 354 | if (naturalHeight && naturalWidth) { 355 | scale = getScaleToWindowMax(width, naturalWidth, height, naturalHeight, zoomMargin); 356 | } 357 | } 358 | // Get the the coords for center of the viewport 359 | var viewportX = getWindowInnerWidth() / 2; 360 | var viewportY = getWindowInnerHeight() / 2; 361 | // Get the coords for center of the parent item 362 | var childCenterX = left + width / 2; 363 | var childCenterY = top + height / 2; 364 | // Get offset amounts for item coords to be centered on screen 365 | var translateX = (viewportX - childCenterX) / scale; 366 | var translateY = (viewportY - childCenterY) / scale; 367 | // Build transform style, including any original transform 368 | transform = 369 | "scale(" + scale + ") translate(" + translateX + "px," + translateY + "px)" + 370 | (originalTransform ? " " + originalTransform : ''); 371 | } 372 | setAttribute(STYLE, stylePosAbsolute + 373 | styleDisplayBlock + 374 | styleMaxWidth100pct + 375 | styleMaxHeight100pct + 376 | (WIDTH + ":" + width + "px;") + 377 | (HEIGHT + ":" + height + "px;") + 378 | (LEFT + ":" + left + "px;") + 379 | (TOP + ":" + top + "px;") + 380 | (TRANSITION + ":" + TRANSFORM + " " + td + "ms " + styleTransitionTimingFn + ";") + 381 | ("-webkit-" + TRANSFORM + ":" + transform + ";") + 382 | ("-ms-" + TRANSFORM + ":" + transform + ";") + 383 | (TRANSFORM + ":" + transform + ";"), zoomableEl); 384 | }; 385 | var zoom = function () { 386 | if (isImgEl) { 387 | zoomImg(); 388 | } 389 | else { 390 | zoomNonImg(); 391 | } 392 | blur(openBtnEl); 393 | }; 394 | var zoomImg = function () { 395 | if (!targetEl || state !== UNLOADED) 396 | return; 397 | zoomableEl = cloneElement(true, targetEl); 398 | removeAttribute(ID, zoomableEl); 399 | setAttribute(DATA_RMIZ_ZOOMED, '', zoomableEl); 400 | addEventListener(LOAD, handleZoomImgLoad, zoomableEl); 401 | }; 402 | var zoomNonImg = function () { 403 | if (!targetEl || state !== UNLOADED) 404 | return; 405 | zoomableEl = createElement(DIV); 406 | setAttribute(DATA_RMIZ_ZOOMED, '', zoomableEl); 407 | setAttribute(STYLE, styleZoomStart, zoomableEl); 408 | var cloneEl = cloneElement(true, targetEl); 409 | removeAttribute(ID, cloneEl); 410 | setStyleProperty(MAX_WIDTH, NONE, cloneEl); 411 | setStyleProperty(MAX_HEIGHT, NONE, cloneEl); 412 | appendChild(cloneEl, zoomableEl); 413 | handleZoomImgLoad(); 414 | }; 415 | var createModal = function () { 416 | if (!zoomableEl) 417 | return; 418 | var el = createElement(DIV); 419 | setAttribute(ARIA_LABEL, modalText, el); 420 | setAttribute(ARIA_MODAL, TRUE_STR, el); 421 | setAttribute(DATA_RMIZ_OVERLAY, '', el); 422 | setAttribute(ROLE, DIALOG, el); 423 | setAttribute(STYLE, POSITION + ":fixed;" + 424 | styleAllDirsZero + 425 | styleWidth100pct + 426 | styleHeight100pct + 427 | (Z_INDEX_CSS + ":" + zoomZindex + ";"), el); 428 | addEventListener(CLICK, handleModalClick, el); 429 | overlayEl = createElement(DIV); 430 | boundaryDivFirst = createElement(DIV); 431 | setAttribute(TABINDEX, ZERO, boundaryDivFirst); 432 | addEventListener(FOCUS, handleFocusBoundaryDiv, boundaryDivFirst); 433 | boundaryDivLast = createElement(DIV); 434 | setAttribute(TABINDEX, ZERO, boundaryDivLast); 435 | addEventListener(FOCUS, handleFocusBoundaryDiv, boundaryDivLast); 436 | closeBtnEl = createElement(BUTTON); 437 | setAttribute(ARIA_LABEL, closeText, closeBtnEl); 438 | setAttribute(STYLE, styleZoomBtnOut, closeBtnEl); 439 | setAttribute(TYPE, BUTTON, el); 440 | addEventListener(CLICK, handleCloseBtnClick, closeBtnEl); 441 | appendChild(overlayEl, el); 442 | appendChild(boundaryDivFirst, el); 443 | appendChild(closeBtnEl, el); 444 | appendChild(zoomableEl, el); 445 | appendChild(boundaryDivLast, el); 446 | return el; 447 | }; 448 | var ariaHideOtherContent = function () { 449 | if (modalEl) { 450 | forEachSibling(function (el) { 451 | if (isIgnoredElement(el)) 452 | return; 453 | var ariaHiddenValue = getAttribute(ARIA_HIDDEN, el); 454 | if (ariaHiddenValue) { 455 | ariaHiddenSiblings.push([el, ariaHiddenValue]); 456 | } 457 | el.setAttribute(ARIA_HIDDEN, TRUE_STR); 458 | }, modalEl); 459 | } 460 | }; 461 | var ariaResetOtherContent = function () { 462 | if (modalEl) { 463 | forEachSibling(function (el) { 464 | if (isIgnoredElement(el)) 465 | return; 466 | removeAttribute(ARIA_HIDDEN, el); 467 | }, modalEl); 468 | } 469 | ariaHiddenSiblings.forEach(function (_a) { 470 | var el = _a[0], ariaHiddenValue = _a[1]; 471 | if (el) { 472 | setAttribute(ARIA_HIDDEN, ariaHiddenValue, el); 473 | } 474 | }); 475 | ariaHiddenSiblings = []; 476 | }; 477 | var unzoom = function () { 478 | if (state === LOADED) { 479 | blur(closeBtnEl); 480 | ariaResetOtherContent(); 481 | if (zoomableEl) { 482 | addEventListener(TRANSITIONEND, handleUnzoomTransitionEnd, zoomableEl); 483 | } 484 | state = UNLOADING; 485 | setZoomImgStyle(false); 486 | if (overlayEl) { 487 | setStyleProperty(OPACITY, ZERO, overlayEl); 488 | } 489 | } 490 | else { 491 | setZoomImgStyle(false); 492 | } 493 | }; 494 | init(); 495 | return { cleanup: cleanup, update: update }; 496 | }; 497 | // 498 | // STYLING 499 | // 500 | var styleAllDirsZero = TOP + ":0;right:0;bottom:0;" + LEFT + ":0;"; 501 | var styleAppearanceNone = "-webkit-appearance:" + NONE + ";-moz-appearance:" + NONE + ";appearance:" + NONE + ";"; 502 | var styleCursorPointer = CURSOR + ":pointer;"; 503 | var styleCursorZoomIn = styleCursorPointer + (CURSOR + ":-webkit-zoom-in;cursor:zoom-in;"); 504 | var styleCursorZoomOut = styleCursorPointer + (CURSOR + ":-webkit-zoom-out;cursor:zoom-out;"); 505 | var styleDisplayBlock = DISPLAY + ":" + BLOCK + ";"; 506 | var styleFastTap = 'touch-action:manipulation;'; 507 | var styleHeight100pct = HEIGHT + ":" + HUNDRED_PCT + ";"; 508 | var styleMaxHeight100pct = "max-height:" + HUNDRED_PCT + ";"; 509 | var styleMaxWidth100pct = "max-width:" + HUNDRED_PCT + ";"; 510 | var stylePosAbsolute = POSITION + ":" + ABSOLUTE + ";"; 511 | var styleTransitionTimingFn = 'ease'; 512 | var styleVisibilityHidden = VISIBILITY + ":" + HIDDEN + ";"; 513 | var styleWidth100pct = WIDTH + ":" + HUNDRED_PCT + ";"; 514 | var styleZoomBtnBase = stylePosAbsolute + 515 | styleFastTap + 516 | styleAppearanceNone + 517 | ("background:" + NONE + ";") + 518 | ("border:" + NONE + ";") + 519 | (MARGIN + ":0;") + 520 | 'padding:0;'; 521 | var styleZoomBtnIn = styleZoomBtnBase + styleCursorZoomIn; 522 | var styleZoomBtnOut = styleZoomBtnBase + 523 | styleAllDirsZero + 524 | styleHeight100pct + 525 | styleWidth100pct + 526 | styleCursorZoomOut + 527 | (Z_INDEX_CSS + ":1;"); 528 | var styleZoomStart = stylePosAbsolute + styleVisibilityHidden; 529 | // 530 | // HELPERS 531 | // 532 | var SVG_REGEX = /\.svg$/i; 533 | var isEscapeKey = function (e) { return e.key === 'Escape' || e.keyCode === 27; }; 534 | var isIgnoredElement = function (_a) { 535 | var tagName = _a.tagName; 536 | return tagName === 'SCRIPT' || tagName === 'NOSCRIPT' || tagName === 'STYLE'; 537 | }; 538 | 539 | export default ImageZoom; 540 | -------------------------------------------------------------------------------- /src/vendors/use-image-zoom.d.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from 'react'; 2 | import { ImageZoomOpts } from './image-zoom'; 3 | interface UseImageZoom extends ImageZoomOpts { 4 | (opts?: ImageZoomOpts): { 5 | ref: Ref; 6 | }; 7 | } 8 | declare const useImageZoom: UseImageZoom; 9 | export default useImageZoom; 10 | -------------------------------------------------------------------------------- /src/vendors/use-image-zoom.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | import ImageZoom from './image-zoom'; 3 | 4 | var useImageZoom = function (opts) { 5 | var ref = useRef(null); 6 | var savedOpts = useRef(opts); 7 | var imgZoom = useRef(); 8 | useEffect(function () { 9 | var _a; 10 | savedOpts.current = opts; 11 | (_a = imgZoom.current) === null || _a === void 0 ? void 0 : _a.update(savedOpts.current); 12 | }, [opts]); 13 | useEffect(function () { 14 | var el = ref.current; 15 | if (!el) 16 | return; 17 | imgZoom.current = ImageZoom(savedOpts.current, el); 18 | return function () { 19 | var _a; 20 | (_a = imgZoom.current) === null || _a === void 0 ? void 0 : _a.cleanup(); 21 | }; 22 | }, []); 23 | return { ref: ref }; 24 | }; 25 | 26 | export default useImageZoom 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------