├── .gitignore ├── README.md ├── lib ├── components │ ├── dashboard.jsx │ ├── footer.jsx │ ├── layout.jsx │ ├── settings.jsx │ ├── title.jsx │ └── welcome.jsx ├── config-context.js ├── use-dom-clean.js ├── use-natural-size.js └── utils.js ├── next.config.js ├── package.json ├── pages ├── _app.jsx ├── _document.jsx └── index.jsx ├── public ├── assets │ └── og-main.png └── favicon.ico └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | .now 3 | .env 4 | .idea 5 | node_modules 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## img 2 | 3 | Transform images for better display on social site. 4 | 5 |
6 | 7 | [Visit online site to learn more](https://img.unix.bio). 8 | -------------------------------------------------------------------------------- /lib/components/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useRef, useState } from 'react' 2 | import { Button, Dot, Image, Spacer, Tag, useTheme, useToasts, Link } from '@zeit-ui/react' 3 | import { getFileUrl, download, getImageBlob, toCompressed } from '../utils' 4 | import Settings from './settings' 5 | 6 | const Dashboard = ({ file, onBack }) => { 7 | const theme = useTheme() 8 | const [, setToast] = useToasts() 9 | const [loading, setLoading] = useState(false) 10 | const [settings, setSettings] = useState({}) 11 | const [size, setSize] = useState({ width: 700, height: 350, bar: 40 }) 12 | const browserRef = useRef(null) 13 | const imgRef = useRef(null) 14 | const ref = useMemo( 15 | () => settings.style === 'plain' ? imgRef : browserRef, 16 | [settings, imgRef, browserRef], 17 | ) 18 | const url = useMemo(() => getFileUrl(file), [file]) 19 | 20 | const clickHandler = () => { 21 | setLoading(true) 22 | getImageBlob(ref.current) 23 | .then(blob => toCompressed(blob)) 24 | .then(res => res.blob()) 25 | .then(blob => { 26 | setLoading(false) 27 | if (!blob) return setToast({ text: 'Please try again later.' }) 28 | download(blob, file.name) 29 | }) 30 | .catch(err => { 31 | setLoading(false) 32 | setToast({ text: err.message }) 33 | }) 34 | } 35 | 36 | return ( 37 |
38 |
39 |
40 | setSize(size)} 41 | settingsChange={val => setSettings(val)} onBack={onBack} /> 42 |
43 | 44 |
45 | {settings.style === 'plain' ? : ( 46 | 48 | 49 | 50 | )} 51 |
52 |
53 | {settings.style === 'plain' ? ( 54 | 55 | ) : ( 56 | 59 | 60 | 61 | )} 62 |
63 |
64 | 65 |
66 | 67 | 68 | 69 | When you download, image will be compressed automatically. 71 | 72 | 73 | 74 | Any questions?  75 | Click here for feedback. 76 | 77 |
78 | 79 | 139 |
140 | ) 141 | } 142 | 143 | export default Dashboard 144 | -------------------------------------------------------------------------------- /lib/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Code, Link, Spacer, useTheme } from '@zeit-ui/react' 3 | 4 | const Footer = () => { 5 | const theme = useTheme() 6 | 7 | return ( 8 | 29 | ) 30 | } 31 | 32 | export default Footer 33 | -------------------------------------------------------------------------------- /lib/components/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTheme, Link } from '@zeit-ui/react' 3 | 4 | const Layout = ({ children }) => { 5 | const theme = useTheme() 6 | 7 | return ( 8 |
9 |
{children}
10 |
11 |

Sorry, the current page does not provide services for the mobile.

12 |

As a suggestion, you can buy an Apple Mac in an Online Store.

13 |
14 | 40 |
41 | ) 42 | } 43 | 44 | export default Layout 45 | -------------------------------------------------------------------------------- /lib/components/settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react' 2 | import { Button, Checkbox, Input, Select, Slider, Spacer, useInput } from '@zeit-ui/react' 3 | import useNaturalSize from '../use-natural-size' 4 | 5 | const Settings = ({ 6 | url, sizeChange, settingsChange, onBack, 7 | }) => { 8 | const nativeSize = useNaturalSize(url) 9 | const [invert, setInvert] = useState(false) 10 | const [radius, setRadius] = useState(5) 11 | const [ratio, setRatio] = useState(1) 12 | const [style, setStyle] = useState('title') 13 | const { state: title, bindings } = useInput('Demo') 14 | const { state: link, bindings: linkBindings } = useInput('https://img.unix.bio') 15 | const size = useMemo(() => ({ 16 | width: Number.parseInt(`${nativeSize.width * ratio}`), 17 | height: Number.parseInt(`${nativeSize.height * ratio}`), 18 | bar: Number.parseInt(`${40 * ratio}`), 19 | }), [nativeSize, ratio]) 20 | useEffect(() => { 21 | sizeChange(size) 22 | }, [size]) 23 | 24 | useEffect(() => { 25 | settingsChange({ style, title, link, radius, invert }) 26 | }, [title, link, style, radius, invert]) 27 | 28 | return ( 29 |
30 | 35 | 36 | {style === 'title' && Title} 37 | {style === 'link' && Link Address} 38 | {style === 'plain' && ( 39 | <> 40 |

Radius

41 | setRadius(val)} showMarkers /> 42 | 43 | )} 44 | 45 |

Size {size.width > 0 && {size.width} * {size.height}}

46 | setRatio(val)} showMarkers /> 47 | 48 | {style !== 'plain' && ( 49 |
50 | setInvert(e.target.checked)}> 52 | Invert Background 53 | 54 |
55 | )} 56 | 57 |
58 | 59 |
60 | 74 |
75 | ) 76 | } 77 | 78 | export default Settings 79 | -------------------------------------------------------------------------------- /lib/components/title.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Input, Select, Spacer } from '@zeit-ui/react' 3 | 4 | const Title = () => { 5 | const [isLink, setIsLink] = useState(false) 6 | const changeHandler = val => { 7 | setIsLink(val === 'link') 8 | } 9 | 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 | 21 |
22 | ) 23 | } 24 | 25 | export default Title 26 | -------------------------------------------------------------------------------- /lib/components/welcome.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Code, Dot, useTheme, Tooltip, useToasts } from '@zeit-ui/react' 3 | import { getCount } from 'lib/utils' 4 | const limit = 10000000 // 10 M 5 | 6 | const Welcome = ({ onChange }) => { 7 | const theme = useTheme() 8 | const [count, setCount] = useState(0) 9 | const [, setToast] = useToasts() 10 | const changeHandler = event => { 11 | const file = event.target.files[0] 12 | if (file.size > limit) { 13 | return setToast({ text: <>Abort. Image needs to be less than 10M. }) 14 | } 15 | onChange(file) 16 | } 17 | 18 | useEffect(() => { 19 | getCount() 20 | .then(res => res.json()) 21 | .then(({ count }) => setCount(count)) 22 | .catch(e => {}) 23 | }, []) 24 | 25 | return ( 26 |
27 |
28 |

29 | UPLOAD Images. 30 | 33 | 34 |

35 |

And Better Display, Better Social Experience.

36 |
37 | {count !== 0 && ( 38 | {count} requests in total.}> 39 | {count} RQS 40 | 41 | )} 42 |
43 |
44 | 130 |
131 | ) 132 | } 133 | 134 | export default Welcome 135 | -------------------------------------------------------------------------------- /lib/config-context.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const ConfigContext = React.createContext({}) 4 | 5 | const useConfigs = () => React.useContext(ConfigContext) 6 | 7 | export default useConfigs 8 | -------------------------------------------------------------------------------- /lib/use-dom-clean.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | const useDomClean = () => { 4 | useEffect(() => { 5 | document.documentElement.removeAttribute('style') 6 | document.body.removeAttribute('style') 7 | }, []) 8 | } 9 | 10 | export default useDomClean 11 | -------------------------------------------------------------------------------- /lib/use-natural-size.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const imageMap = { 4 | } 5 | 6 | const useNaturalSize = (url) => { 7 | const [size, setSize] = useState({ width: 0, height: 0 }) 8 | useEffect(() => { 9 | if (imageMap[url]) return setSize(imageMap[url]) 10 | const id = `image-${Math.random().toString(32).slice(2, 10)}` 11 | const img = document.createElement('img') 12 | img.src = url 13 | img.id = id 14 | img.style.cssText = 'position: fixed; bottom:-9000px; left:-9000px; width:auto; height:auto;' 15 | img.onload = () => { 16 | const width = img.naturalWidth || img.width 17 | const height = img.naturalHeight || img.height 18 | const ratio = width / 700 19 | const size = { 20 | width: 700, 21 | height: height / ratio, 22 | } 23 | imageMap[url] = size 24 | setSize(size) 25 | document.documentElement.removeChild(document.getElementById(id)) 26 | } 27 | document.documentElement.appendChild(img) 28 | }) 29 | 30 | return size 31 | } 32 | 33 | export default useNaturalSize 34 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | export const getFileUrl = file => { 2 | if (typeof window.createObjectURL !== 'undefined') { 3 | return window.createObjectURL(file) 4 | } 5 | 6 | if (typeof window.URL !== 'undefined') { 7 | return window.URL.createObjectURL(file) 8 | } 9 | 10 | if (typeof window.webkitURL !== 'undefined') { 11 | return window.webkitURL.createObjectURL(file) 12 | } 13 | 14 | return null 15 | } 16 | 17 | export const toBlob = (data, type) => { 18 | const bytes = window.atob(data) 19 | let n = bytes.length 20 | const u8arr = new Uint8Array(n) 21 | while (n--) u8arr[n] = bytes.charCodeAt(n) 22 | return new Blob([u8arr], { type }) 23 | } 24 | 25 | export const getImageBlob = async (el) => { 26 | const { default: html2canvas } = await import('html2canvas') 27 | const canvas = await html2canvas(el, { 28 | allowTaint: true, 29 | scale: 1, 30 | backgroundColor: null, 31 | }) 32 | const base64 = canvas.toDataURL('image/png', 1) 33 | const data = base64.split('base64,').pop() 34 | return toBlob(data, 'image/png') 35 | } 36 | 37 | export const toCompressed = async blob => { 38 | return fetch('https://min.unix.bio/api/png', { 39 | method: 'POST', 40 | mode: 'cors', 41 | headers: { 'Content-Type': 'image/png' }, 42 | body: blob, 43 | }) 44 | } 45 | 46 | export const getCount = async () => { 47 | return fetch('https://min.unix.bio/api/count', { 48 | method: 'GET', 49 | mode: 'cors', 50 | }) 51 | } 52 | 53 | export const download = (blob, name = 'display.png') => { 54 | const suffix = name.split('.').reverse()[0] 55 | const filename = name.replace(`.${suffix}`, '') 56 | const reader = new FileReader() 57 | reader.readAsDataURL(blob) 58 | reader.onloadend = () => { 59 | const a = document.createElement('a') 60 | a.href = reader.result 61 | a.download = `${filename}.png` 62 | a.click() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withMDX = require('@next/mdx')({ 2 | extension: /\.(md|mdx)?$/, 3 | options: { 4 | rehypePlugins: [require('rehype-join-line')], 5 | }, 6 | }) 7 | 8 | const nextConfig = { 9 | target: 'serverless', 10 | 11 | pageExtensions: ['jsx', 'js', 'mdx', 'md', 'ts', 'tsx'], 12 | 13 | cssModules: true, 14 | 15 | cssLoaderOptions: { 16 | importLoaders: 1, 17 | localIdentName: '[local]___[hash:base64:5]', 18 | }, 19 | 20 | env: { 21 | VERSION: require('./package.json').version, 22 | }, 23 | 24 | webpack(config) { 25 | config.resolve.modules.push(__dirname) 26 | return config 27 | }, 28 | } 29 | 30 | module.exports = withMDX(nextConfig) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "img", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:unix/img.git", 6 | "author": "unix ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "next dev", 10 | "build": "next build" 11 | }, 12 | "dependencies": { 13 | "@zeit-ui/react": "^0.0.1-beta.32", 14 | "html2canvas": "^1.0.0-rc.5", 15 | "next": "^9.3.5", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1" 18 | }, 19 | "devDependencies": { 20 | "@mdx-js/loader": "^1.5.8", 21 | "@next/mdx": "^9.3.5", 22 | "rehype-join-line": "^1.0.2", 23 | "styled-jsx": "^3.2.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import useDomClean from 'lib/use-dom-clean' 3 | import React from 'react' 4 | import { ZEITUIProvider, CSSBaseline } from '@zeit-ui/react' 5 | 6 | const Application = ({ Component, pageProps }) => { 7 | useDomClean() 8 | 9 | return ( 10 | <> 11 | 12 | Better social image 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export default Application 45 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' 2 | import { CSSBaseline } from '@zeit-ui/react' 3 | import flush from 'styled-jsx/server' 4 | 5 | class MyDocument extends Document { 6 | static async getInitialProps (ctx) { 7 | const initialProps = await Document.getInitialProps(ctx) 8 | const styles = CSSBaseline.flush() 9 | 10 | return { 11 | ...initialProps, 12 | styles: ( 13 | <> 14 | {initialProps.styles} 15 | {styles} 16 | {flush()} 17 | 18 | ) 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 | 26 | 27 |