Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module/how to #473

Merged
merged 10 commits into from
May 7, 2019
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ deploy:
skip_cleanup: true
on:
branch: production
tags: true
after_deploy:
- echo "REACT_APP_BRANCH=$REACT_APP_BRANCH"
5 changes: 2 additions & 3 deletions src/components/Icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
MdAccountCircle,
MdLock,
MdClose,
MdDelete,
MdMoreVert,
MdComment,
MdTurnedIn,
Expand All @@ -28,7 +27,7 @@ import {
MdImage,
MdArrowForward,
} from 'react-icons/md'
import { GoCloudUpload, GoFilePdf } from 'react-icons/go'
import { GoCloudUpload, GoFilePdf, GoTrashcan } from 'react-icons/go'
import { FaSignal } from 'react-icons/fa'
import { IconContext } from 'react-icons'
import SVGs from './svgs'
Expand Down Expand Up @@ -82,7 +81,7 @@ export const glyphs: IGlyphs = {
'account-circle': <MdAccountCircle />,
lock: <MdLock />,
close: <MdClose />,
delete: <MdDelete />,
delete: <GoTrashcan />,
'more-vert': <MdMoreVert />,
comment: <MdComment />,
'turned-in': <MdTurnedIn />,
Expand Down
166 changes: 166 additions & 0 deletions src/components/ImageInput/ImageConverter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as React from 'react'
import { FlexContainer } from '../Layout/FlexContainer'
import { Button } from '../Button'
import * as clientCompress from 'client-compress'
import { IConvertedFileMeta, bytesToSize } from './ImageInput'
import Text from '../Text'

interface IProps {
file: File
onImgConverted: (meta: IConvertedFileMeta) => void
onImgClicked: (meta: IConvertedFileMeta) => void
}
interface IState {
imageQuality: ImageQualities
convertedFile?: IConvertedFileMeta
openLightbox?: boolean
}

type ImageQualities = 'normal' | 'high' | 'low'
const imageSizes = {
low: 640,
normal: 1280,
high: 1920,
}

export class ImageConverter extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps>
private compressionOptions = {
quality: 0.75,
maxWidth: imageSizes.normal,
}
private compress: clientCompress = new clientCompress(this.compressionOptions)

constructor(props: IProps) {
super(props)
this.state = { imageQuality: 'normal' }
}

componentDidMount() {
// call on mount to trigger initial conversion when converter created
this.setImageQuality('normal')
}

componentWillUnmount() {
// Revoke the object URL to free up memory
if (this.state.convertedFile) {
URL.revokeObjectURL(this.state.convertedFile.objectUrl)
}
}

async setImageQuality(quality: ImageQualities) {
this.setState({
imageQuality: quality,
})
this.compressionOptions.maxWidth = imageSizes[quality]
this.compress = new clientCompress(this.compressionOptions)
await this.compressFiles(this.props.file)
}

async compressFiles(file: File) {
// by default compress takes an array and gives back an array. We only want to handle a single image
const conversion: ICompressedOutput[] = await this.compress.compress([file])
const convertedMeta = this._generateFileMeta(conversion[0])
this.setState({
convertedFile: convertedMeta,
})
this.props.onImgConverted(convertedMeta)
}

private _generateFileMeta(c: ICompressedOutput) {
const meta: IConvertedFileMeta = {
name: c.photo.name,
startSize: bytesToSize(c.info.startSizeMB * 1000 * 1000),
endSize: bytesToSize(c.info.endSizeMB * 1000 * 1000),
compressionPercent: Number(c.info.sizeReducedInPercent.toFixed(1)),
photoData: c.photo.data,
objectUrl: URL.createObjectURL(c.photo.data),
type: c.photo.type,
}
return meta
}

render() {
const { convertedFile, imageQuality } = this.state
const qualities: ImageQualities[] = ['low', 'normal', 'high']

return convertedFile ? (
<div>
<div
style={{
backgroundImage: `url(${convertedFile.objectUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
height: '220px',
border: '1px solid #dddddd',
}}
id="preview"
onClick={() => this.props.onImgClicked(convertedFile)}
/>
<div>
<FlexContainer p={0} bg="none" mt={2} mb={2}>
{convertedFile &&
qualities.map(quality => (
<Button
variant={imageQuality === quality ? 'dark' : 'outline'}
key={quality}
onClick={() => this.setImageQuality(quality)}
>
{quality}
</Button>
))}
</FlexContainer>
<div>
{convertedFile.startSize} -> {convertedFile.endSize}
</div>
<Text small>{convertedFile.compressionPercent}% smaller 🌍</Text>
</div>
</div>
) : null
}
}
ImageConverter.defaultProps = {
onImgClicked: () => null,
}

/************************************************************************************
* Interfaces
*
*************************************************************************************/

interface ICompressedOutput {
photo: ICompressedPhoto
info: ICompressedInfo
}

interface ICompressedPhoto {
name: string
type: 'image/jpeg' | string
size: number // in bytes,
orientation: -1
data: Blob
width: number
height: number
}
// This is the metadata for this conversion
interface ICompressedInfo {
start: number
quality: number
startType: 'image/jpeg'
startWidth: number
startHeight: number
endWidth: number
endHeight: number
iterations: number
startSizeMB: number
endSizeMB: number
sizeReducedInPercent: number
end: number
elapsedTimeInSeconds: number
endType: 'image/jpeg'
}

type imageFormats = 'image/jpeg' | 'image/jpg' | 'image/gif' | 'image/png'
// NOTE - gifs will lose animation and png will lost transparency
// Additional types: image/bmp, image/tiff, image/x-icon, image/svg+xml, image/webp, image/xxx
Loading