Avatar upload

This commit is contained in:
Jori Lallo
2017-12-13 23:17:08 -08:00
parent c2879c51b2
commit 7d756e4fae
7 changed files with 228 additions and 20 deletions

View File

@@ -3,7 +3,7 @@ import { Change } from 'slate';
import { Editor } from 'slate-react';
import uuid from 'uuid';
import EditList from './plugins/EditList';
import uploadFile from 'utils/uploadFile';
import { uploadFile } from 'utils/uploadFile';
const { changes } = EditList;

View File

@@ -9,6 +9,7 @@ import { color, size } from 'shared/styles/constants';
import { client } from 'utils/ApiClient';
import AuthStore from 'stores/AuthStore';
import ErrorsStore from 'stores/ErrorsStore';
import ImageUpload from './components/ImageUpload';
import Input, { LabelText } from 'components/Input';
import Button from 'components/Button';
import CenteredContent from 'components/CenteredContent';
@@ -24,6 +25,7 @@ class Settings extends Component {
};
@observable name: string;
@observable avatarUrl: ?string;
@observable updated: boolean;
@observable isSaving: boolean;
@@ -44,6 +46,7 @@ class Settings extends Component {
try {
const res = await client.post(`/user.update`, {
name: this.name,
avatarUrl: this.avatarUrl,
});
invariant(res && res.data, 'Document list not available');
const { data } = res;
@@ -63,10 +66,18 @@ class Settings extends Component {
this.name = ev.target.value;
};
handleAvatarUpload = (avatarUrl: string) => {
this.avatarUrl = avatarUrl;
};
handleAvatarError = (error: ?string) => {
this.props.errors.add(error || 'Unable to upload new avatar');
};
render() {
const { user } = this.props.auth;
if (!user) return null;
const avatarUrl = user.avatarUrl;
const avatarUrl = this.avatarUrl || user.avatarUrl;
return (
<CenteredContent>
@@ -75,14 +86,19 @@ class Settings extends Component {
<ProfilePicture column>
<LabelText>Profile picture</LabelText>
<AvatarContainer>
<Avatar src={avatarUrl} />
<Flex auto align="center" justify="center">
Upload new image
</Flex>
<ImageUpload
onSuccess={this.handleAvatarUpload}
onError={this.handleAvatarError}
>
<Avatar src={avatarUrl} />
<Flex auto align="center" justify="center">
Upload new image
</Flex>
</ImageUpload>
</AvatarContainer>
</ProfilePicture>
<form onSubmit={this.handleSubmit}>
<Input
<StyledInput
label="Name"
value={this.name}
onChange={this.handleNameChange}
@@ -122,7 +138,7 @@ const AvatarContainer = styled(Flex)`
${avatarStyles};
position: relative;
div {
div div {
${avatarStyles};
position: absolute;
top: 0;
@@ -145,4 +161,8 @@ const Avatar = styled.img`
${avatarStyles};
`;
const StyledInput = styled(Input)`
max-width: 350px;
`;
export default inject('auth', 'errors', 'auth')(Settings);

View File

@@ -0,0 +1,143 @@
// @flow
import React, { Component } from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import styled from 'styled-components';
import Dropzone from 'react-dropzone';
import LoadingIndicator from 'components/LoadingIndicator';
import Flex from 'shared/components/Flex';
import Modal from 'components/Modal';
import Button from 'components/Button';
import AvatarEditor from 'react-avatar-editor';
import { uploadFile, dataUrlToBlob } from 'utils/uploadFile';
type Props = {
children?: React$Element<any>,
onSuccess: string => void,
onError: string => void,
};
@observer
class DropToImport extends Component {
@observable isUploading: boolean = false;
@observable isCropping: boolean = false;
@observable zoom: number = 1;
props: Props;
file: File;
avatarEditorRef: HTMLCanvasElement;
onDropAccepted = async (files: File[]) => {
this.isCropping = true;
this.file = files[0];
};
handleCrop = async () => {
// $FlowIssue getImage() exists
const canvas = this.avatarEditorRef.getImage();
const imageBlob = dataUrlToBlob(canvas.toDataURL());
try {
const asset = await uploadFile(imageBlob, { name: this.file.name });
this.props.onSuccess(asset.url);
} catch (err) {
this.props.onError('Unable to upload image');
} finally {
this.isUploading = false;
this.isCropping = false;
}
};
handleZoom = (event: SyntheticDragEvent) => {
let target = event.target;
if (target instanceof HTMLInputElement) {
this.zoom = parseFloat(target.value);
}
};
renderCropping() {
return (
<Modal isOpen title="">
<Flex auto column align="center" justify="center">
<AvatarEditorContainer>
<AvatarEditor
ref={ref => (this.avatarEditorRef = ref)}
image={this.file}
width={250}
height={250}
border={25}
borderRadius={150}
color={[255, 255, 255, 0.6]} // RGBA
scale={this.zoom}
rotate={0}
/>
</AvatarEditorContainer>
<RangeInput
type="range"
min="0.1"
max="2"
step="0.01"
defaultValue="1"
onChange={this.handleZoom}
/>
{this.isUploading && <LoadingIndicator />}
<CropButton onClick={this.handleCrop} disabled={this.isUploading}>
Crop avatar
</CropButton>
</Flex>
</Modal>
);
}
render() {
if (this.isCropping) {
return this.renderCropping();
} else {
return (
<Dropzone
accept="image/png, image/jpeg"
onDropAccepted={this.onDropAccepted}
style={{}}
disablePreview
{...this.props}
>
{this.props.children}
</Dropzone>
);
}
}
}
const AvatarEditorContainer = styled(Flex)`
margin-bottom: 30px;
`;
const RangeInput = styled.input`
display: block;
width: 300px;
margin-bottom: 30px;
height: 4px;
cursor: pointer;
color: inherit;
border-radius: 99999px;
background-color: #dee1e3;
appearance: none;
&::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: black;
cursor: pointer;
}
&:focus {
outline: none;
}
`;
const CropButton = styled(Button)`
width: 300px;
`;
export default DropToImport;

View File

@@ -6,15 +6,22 @@ type File = {
blob: boolean,
type: string,
size: number,
name: string,
name?: string,
file: string,
};
export default async function uploadFile(file: File) {
type Options = {
name?: string,
};
export const uploadFile = async (file: File | Blob, option?: Options) => {
// $FlowFixMe Blob makes life hard
const filename = (option && option.name) || file.name;
const response = await client.post('/user.s3Upload', {
kind: file.type,
size: file.size,
filename: file.name,
filename,
});
invariant(response, 'Response should be available');
@@ -28,6 +35,7 @@ export default async function uploadFile(file: File) {
}
if (file.blob) {
// $FlowFixMe
formData.append('file', file.file);
} else {
// $FlowFixMe
@@ -41,4 +49,14 @@ export default async function uploadFile(file: File) {
await fetch(data.uploadUrl, options);
return asset;
}
};
export const dataUrlToBlob = (dataURL: string) => {
var blobBin = atob(dataURL.split(',')[1]);
var array = [];
for (var i = 0; i < blobBin.length; i++) {
array.push(blobBin.charCodeAt(i));
}
const file = new Blob([new Uint8Array(array)], { type: 'image/png' });
return file;
};