├── .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 |
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 |
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 |
36 |
37 |
38 |
39 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | export default MyDocument
57 |
--------------------------------------------------------------------------------
/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import Layout from 'lib/components/layout'
2 | import Dashboard from 'lib/components/dashboard'
3 | import Welcome from 'lib/components/welcome'
4 | import Footer from 'lib/components/footer'
5 | import { useState } from 'react'
6 |
7 | const Index = () => {
8 | const [file, setFile] = useState(null)
9 |
10 | return (
11 |
12 | {file ? (
13 | setFile(null)} />
14 | ) : setFile(file)} />}
15 |
16 |
17 | )
18 | }
19 |
20 | export default Index
21 |
--------------------------------------------------------------------------------
/public/assets/og-main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unix/img/46efe7ab08f20e2aa25214edad8ac0511ed4307f/public/assets/og-main.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unix/img/46efe7ab08f20e2aa25214edad8ac0511ed4307f/public/favicon.ico
--------------------------------------------------------------------------------