Save uploaded avatar to team

This commit is contained in:
Barry Low committed 2018-09-20 17:51:41 -07:00
1 parent ac5e253d02
commit ecc538648e
14 files changed
+255 -184

No files matched your search

+1
View File
@@ -41,6 +41,7 @@ type TeamProfile {
description: String
motto: String
avatar: String
banner: String
updatedAt: DateTime!
createdAt: DateTime!
}
+74 -3
View File
@@ -1,5 +1,5 @@
# source: http://localhost:4466
# timestamp: Wed Sep 12 2018 08:41:57 GMT-0700 (Pacific Daylight Time)
# timestamp: Thu Sep 20 2018 16:52:44 GMT-0700 (Pacific Daylight Time)
type AggregateFBStatus {
count: Int!
@@ -1400,7 +1400,9 @@ type TeamProfile {
description: String
motto: String
avatar: String
banner: String
updatedAt: DateTime!
createdAt: DateTime!
}
"""A connection to a list of items."""
@@ -1417,6 +1419,7 @@ input TeamProfileCreateInput {
description: String
motto: String
avatar: String
banner: String
}
input TeamProfileCreateOneInput {
@@ -1439,19 +1442,23 @@ enum TeamProfileOrderByInput {
motto_DESC
avatar_ASC
avatar_DESC
banner_ASC
banner_DESC
updatedAt_ASC
updatedAt_DESC
id_ASC
id_DESC
createdAt_ASC
createdAt_DESC
id_ASC
id_DESC
}
type TeamProfilePreviousValues {
description: String
motto: String
avatar: String
banner: String
updatedAt: DateTime!
createdAt: DateTime!
}
type TeamProfileSubscriptionPayload {
@@ -1497,12 +1504,14 @@ input TeamProfileUpdateDataInput {
description: String
motto: String
avatar: String
banner: String
}
input TeamProfileUpdateInput {
description: String
motto: String
avatar: String
banner: String
}
input TeamProfileUpdateOneInput {
@@ -1647,6 +1656,46 @@ input TeamProfileWhereInput {
"""All values not ending with the given string."""
avatar_not_ends_with: String
banner: String
"""All values that are not equal to given value."""
banner_not: String
"""All values that are contained in given list."""
banner_in: [String!]
"""All values that are not contained in given list."""
banner_not_in: [String!]
"""All values less than the given value."""
banner_lt: String
"""All values less than or equal the given value."""
banner_lte: String
"""All values greater than the given value."""
banner_gt: String
"""All values greater than or equal the given value."""
banner_gte: String
"""All values containing the given string."""
banner_contains: String
"""All values not containing the given string."""
banner_not_contains: String
"""All values starting with the given string."""
banner_starts_with: String
"""All values not starting with the given string."""
banner_not_starts_with: String
"""All values ending with the given string."""
banner_ends_with: String
"""All values not ending with the given string."""
banner_not_ends_with: String
updatedAt: DateTime
"""All values that are not equal to given value."""
@@ -1669,6 +1718,28 @@ input TeamProfileWhereInput {
"""All values greater than or equal the given value."""
updatedAt_gte: DateTime
createdAt: DateTime
"""All values that are not equal to given value."""
createdAt_not: DateTime
"""All values that are contained in given list."""
createdAt_in: [DateTime!]
"""All values that are not contained in given list."""
createdAt_not_in: [DateTime!]
"""All values less than the given value."""
createdAt_lt: DateTime
"""All values less than or equal the given value."""
createdAt_lte: DateTime
"""All values greater than the given value."""
createdAt_gt: DateTime
"""All values greater than or equal the given value."""
createdAt_gte: DateTime
}
type TeamStatus {
+1
View File
@@ -13,6 +13,7 @@ const TEAMS_FRAGMENT = gql`{
createdAt
profile {
avatar
banner
description
motto
updatedAt
+7 -6
View File
@@ -121,8 +121,8 @@ const resolvers = {
return err
})
},
async updateTeamProfile(parent, { slug, owner, description, motto, avatar }, ctx, info) {
return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar}}}, where:{slug: slug}}, TEAMS_FRAGMENT)
async updateTeamProfile(parent, { slug, owner, description, motto, avatar, banner }, ctx, info) {
return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar, banner: banner}}}, where:{slug: slug}}, TEAMS_FRAGMENT)
.catch((err) => {
console.log('createTeam error: ', err)
return err
@@ -140,8 +140,8 @@ const resolvers = {
async getAzureSAS(parent, { teamSlug }, ctx, info) {
// Create start and expiry times
let today = new Date()
let tomorrow = new Date()
tomorrow.setDate(today.getDate() + 7)
let week = new Date()
week.setDate(today.getDate() + 7)
// Create SharedKeyCredential and attach to pipline
const SKC = new Azure.SharedKeyCredential(azureAccount, azureKey)
const pipeline = Azure.StorageURL.newPipeline(SKC)
@@ -179,10 +179,11 @@ const resolvers = {
// Generate SAS url
const SASQueryParameters = Azure.generateAccountSASQueryParameters(
{
containerName,
version: '2017-11-09',
permissions: SASContainerPerms,
startTime: today,
expiryTime: tomorrow,
expiryTime: week,
protocol: 'https',
services: SASServicePerms,
resourceTypes: SASResourceTypes
}, SKC )
+1 -6
View File
@@ -19,16 +19,11 @@ type Query {
type Mutation {
authenticate(idToken: String!): Member
createTeam(name: String!, slug: String!, owner: String!): Team @isAuthenticated
updateTeamProfile(slug: String!, owner: String!, description: String, motto: String, avatar: String): Team @isAuthenticated
updateTeamProfile(slug: String!, owner: String!, description: String, motto: String, avatar: String, banner: String): Team @isAuthenticated
deleteTeam(slug: String!): Team @isAuthenticated @isOwner(type: "Team")
getAzureSAS(teamSlug: String!):String
}
input MemberProfileInput {
authId: String!
alias: String!
}
fragment MemberInfo on Member {
id
authId
+5
View File
@@ -4847,6 +4847,11 @@
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browser-image-resizer": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browser-image-resizer/-/browser-image-resizer-1.2.0.tgz",
"integrity": "sha512-xMiB6BnRsD6RWmkD6Av90qXY7uaAOXJPaS5D8cSazyao/bx6ENCm67XGl6BIhn1ETjK52yEs8A/atmMPp74M4A=="
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+1
View File
@@ -82,6 +82,7 @@
"azure-storage": "^2.10.1",
"babel-polyfill": "^6.26.0",
"blob-util": "^2.0.2",
"browser-image-resizer": "^1.2.0",
"classnames": "^2.2.6",
"cross-fetch": "^2.2.2",
"dotenv-webpack": "^1.5.7",
@@ -18,6 +18,7 @@ export const GET_ALL_TEAMS_QUERY = gql`
createdAt
profile {
avatar
banner
description
motto
updatedAt
@@ -16,6 +16,7 @@ export const GET_TEAMS_BY_OWNER = gql`
createdAt
profile {
avatar
banner
description
motto
updatedAt
+1
View File
@@ -15,6 +15,7 @@ export const TEAMS_FRAGMENT = gql`
createdAt
profile {
avatar
banner
description
motto
updatedAt
@@ -3,16 +3,20 @@ import gql from 'graphql-tag'
const UPDATE_TEAM_PROFILE = gql`
mutation UpdateTeamProfileMutation(
$slug: String!
$owner: String!
$description: String
$motto: String
$slug: String!,
$owner: String!,
$description: String,
$motto: String,
$avatar: String,
$banner: String
) {
updateTeamProfile(
slug: $slug
owner: $owner
description: $description
slug: $slug,
owner: $owner,
description: $description,
motto: $motto
avatar: $avatar,
banner: $banner
) {
id
name
@@ -26,6 +30,7 @@ const UPDATE_TEAM_PROFILE = gql`
createdAt
profile {
avatar
banner
description
motto
updatedAt
+99 -133
View File
@@ -4,6 +4,7 @@ import { Mutation } from 'react-apollo'
import Dropzone from 'react-dropzone'
import { Cropper } from 'react-image-cropper'
import * as Azure from '@azure/storage-blob'
import readAndCompressImage from 'browser-image-resizer'
import { imgSrcToBlob } from 'blob-util'
import { withStyles } from '@material-ui/core/styles'
@@ -12,6 +13,8 @@ import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import Button from '@material-ui/core/Button'
import MessageCard from './MessageCard'
import GET_AZURE_SAS from '../../graphql/teams/GetAzureSASMutation'
const styles = theme => ({
@@ -45,16 +48,21 @@ const styles = theme => ({
}
})
const imageConfig = {
quality: 0.5,
maxWidth: 200,
maxHeight: 200,
autoRotate: true,
debug: true,
mimeType: 'image/jpeg'
}
class UploadImage extends Component {
constructor (props) {
super(props)
// this.saveToStorage = this.saveToStorage.bind(this)
this.onDrop = this.onDrop.bind(this)
this.handleCrop = this.handleCrop.bind(this)
this.onCropComplete = this.onCropComplete.bind(this)
this.onCropChange = this.onCropChange.bind(this)
this.getCroppedImg = this.getCroppedImg.bind(this)
this.handleClickOpen = this.handleClickOpen.bind(this)
this.handleClose = this.handleClose.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
@@ -70,17 +78,27 @@ class UploadImage extends Component {
width: 200,
height: 200
},
cropping: false,
cropped: null,
rejected: null
rejected: null,
uploaded: false,
image: ''
}
}
render () {
const { classes, team, authId } = this.props
console.log('UploadImage', team, authId, this.state)
const { classes, team, authId, handleAvatar } = this.props
let avatar = null
if (team.profile !== null && team.profile.avatar !== undefined && team.profile.avatar !== null && team.profile.avatar !== 'a') {
avatar = team.profile.avatar
} else {
if (this.state.image !== '') avatar = this.state.image
}
console.log('UploadImage', team, avatar, authId, this.state)
return (
<div className={classes.dropzoneContainer}>
{this.state.src === null && (
{avatar === null && this.state.src === null && (
<Dropzone
accept='image/jpeg, image/png'
onDrop={this.onDrop}
@@ -90,55 +108,64 @@ class UploadImage extends Component {
<p>Drag-and-drop team avater here or click to select files. <br /> Only *.jpeg and *.png images</p>
</Dropzone>
)}
{this.state.cropped !== null && (
{(avatar !== null || this.state.src !== null) && (
<Mutation mutation={GET_AZURE_SAS} >
{(getAzureSAS, { loading, error, data }) => {
console.log('getAzureSAS: ', error, data)
console.log('getAzureSAS keys: ', process.env.AZURE_STORAGE_URL, process.env.AZURE_SAS)
if (data !== undefined && data.getAzureSAS !== null) {
this.saveAvatar(data.getAzureSAS, team.slug)
if (!this.state.uploaded) {
if (data !== undefined && data.getAzureSAS !== null) {
this.saveAvatar(data.getAzureSAS, team.slug, handleAvatar)
}
// this.handleSubmit(getAzureSAS, team.slug)
}
if (this.state.cropping) {
return (
<div>
<MessageCard message='Cropping...' />
</div>
)
} else {
return (
<React.Fragment>
<div>
<img src={avatar} className={classes.cropPreview} alt='Crop Preview' />
</div>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby='form-dialog-title'
>
<div classes={classes.dialogContainer}>
<DialogContent>
<div className={classes.cropBox}>
<Cropper
src={this.state.src}
originX={100}
originY={100}
ref={ref => { this.cropper = ref }}
onImgLoad={this.onImageLoaded}
/>
</div>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color='primary'>
Cancel
</Button>
<Button
onClick={(e) => this.handleCrop(e, getAzureSAS, team.slug, handleAvatar)}
color='primary'
>
Crop Image
</Button>
</DialogActions>
</div>
</Dialog>
</React.Fragment>
)
}
return (
<div>
<img src={this.state.cropped} className={classes.cropPreview} alt='Crop Preview' />
<Button onClick={(e) => this.handleSubmit(e, getAzureSAS, team.slug)}>Save</Button>
</div>
)
}}
</Mutation>
)}
{this.state.src !== null && (
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby='form-dialog-title'
>
<div classes={classes.dialogContainer}>
<DialogContent>
<div className={classes.cropBox}>
<Cropper
src={this.state.src}
originX={100}
originY={100}
ref={ref => { this.cropper = ref }}
onImgLoad={this.onImageLoaded}
/>
</div>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color='primary'>
Cancel
</Button>
<Button
onClick={this.handleCrop}
color='primary'
>
Crop Image
</Button>
</DialogActions>
</div>
</Dialog>
)}
</div>
)
}
@@ -167,29 +194,30 @@ class UploadImage extends Component {
}
}
handleCrop () {
console.log('handleCrop', this.cropper)
this.setState({ cropped: this.cropper.crop(), open: false })
async handleCrop (e, getAzureSAS, teamSlug, handleAvatar) {
e.preventDefault()
const data = await getAzureSAS({ variables: { teamSlug } })
console.log('handleCrop', this.cropper, data)
const cropped = this.cropper.crop()
const croppedBlob = await imgSrcToBlob(cropped, 'image/png', 'Anonymous')
let resizedBlob = await readAndCompressImage(croppedBlob, imageConfig)
return this.setState({ cropped: resizedBlob, cropping: true, open: false }, () => {
this.saveAvatar(data.getAzureSAS, teamSlug, handleAvatar)
})
}
async handleSubmit (e, getAzureSAS, teamSlug) {
e.preventDefault()
await getAzureSAS({ variables: { teamSlug } })
}
async saveAvatar (SASQueryParameters, teamSlug) {
const blob = await imgSrcToBlob(this.state.cropped, 'image/png', 'Anonymous')
console.log('handleSubmit0:', await blob, await blob.type)
const type = blob.type
let ext
if (type === 'image/jpeg') ext = 'jpg'
if (type === 'image/png') ext = 'png'
const fileName = `${teamSlug}-avatar.${ext}`
async saveAvatar (SASQueryParameters, teamSlug, handleAvatar) {
const fileName = `${teamSlug}-avatar.jpg`
console.log('saveAvatar SASurl', process.env.AZURE_SAS_URL)
// const SASurl = `${process.env.AZURE_STORAGE_URL}?${SASQueryParameters}`
const SASurl2 = `${process.env.AZURE_STORAGE_URL}?${SASQueryParameters}`
const SASurl = process.env.AZURE_SAS_URL
console.log('getAzureSAS2 SASurl: ', SASurl2, SASurl)
const pipeline = Azure.StorageURL.newPipeline(new Azure.AnonymousCredential())
const serviceURL = new Azure.ServiceURL(SASurl, pipeline)
console.log('getAzureSAS2: ', serviceURL)
@@ -198,54 +226,15 @@ class UploadImage extends Component {
console.log('getAzureSAS3: ', containerURL)
const blockBlobURL = Azure.BlockBlobURL.fromContainerURL(containerURL, fileName)
console.log(Azure.Aborter.None, await blob, blockBlobURL)
const avatar = await Azure.uploadBrowserDataToBlockBlob(Azure.Aborter.None, blob, blockBlobURL)
console.log('getAzureSAS34: ', blockBlobURL)
const avatar = await Azure.uploadBrowserDataToBlockBlob(Azure.Aborter.None, this.state.cropped, blockBlobURL)
console.log('saveAvatar: ', avatar)
const image = `${process.env.AZURE_STORAGE_URL}${teamSlug}/${fileName}`
handleAvatar(image)
this.setState({ cropping: false, uploaded: true, image: image })
return avatar
}
onCropComplete (crop, pixelCrop) {
console.log('onCropComplete', crop, pixelCrop, this.state)
const cropped = this.getCroppedImg(crop, pixelCrop, this.state.src)
this.setState({ cropped })
}
onCropChange (crop) {
this.setState({ crop })
}
getCroppedImg (image, pixelCrop, fileName) {
const canvas = document.createElement('canvas')
canvas.width = pixelCrop.width
canvas.height = pixelCrop.height
const ctx = canvas.getContext('2d')
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height
)
// As Base64 string
return canvas.toDataURL('image/jpeg')
// As a blob
/*
return new Promise((resolve, reject) => {
canvas.toBlob(file => {
file.name = fileName
resolve(file)
}, 'image/jpeg')
});
*/
}
handleClickOpen () {
this.setState({ open: true })
}
@@ -253,36 +242,13 @@ class UploadImage extends Component {
handleClose () {
this.setState({ open: false, src: null })
}
/*
async saveToStorage (file) {
const blobService = Azure.createBlobService()
const container = ''
await uploadBrowserDataToBlockBlob(Aborter.None, file, blockBlobURL, {
blockSize: 4 * 1024 * 1024, // 4MB block size
parallelism: 20, // 20 concurrency
progress: ev => console.log(ev)
});
}
*/
}
UploadImage.propTypes = {
classes: PropTypes.object.isRequired,
team: PropTypes.object,
authId: PropTypes.string.isRequired
authId: PropTypes.string.isRequired,
handleAvatar: PropTypes.func.isRequired
}
export default withStyles(styles)(UploadImage)
/*
<ReactCrop
src={this.state.src}
crop={this.state.crop}
onImageLoaded={this.onImageLoaded}
onComplete={this.onCropComplete}
onChange={this.onCropChange}
className={classes.reactCrop}
imageStyle={{ display: 'block', maxWidth: '100%', maxHeight: 'stretch' }}
/>
*/
@@ -41,13 +41,18 @@ export class ManageTeamEdit extends Component {
this.handleClose = this.handleClose.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleAvatar = this.handleAvatar.bind(this)
console.log('ManageTeamEdit', props.team)
const motto = props.team.profile.motto || ''
const description = props.team.profile.description || ''
const avatar = props.team.profile.avatar || ''
const banner = props.team.profile.banner || ''
this.state = {
open: false,
motto: motto,
description: description
description: description,
avatar: avatar,
banner: banner
}
}
@@ -103,7 +108,7 @@ export class ManageTeamEdit extends Component {
Edit Team Details
</DialogTitle>
<DialogContent>
<UploadImage team={team} authId={authId} />
<UploadImage team={team} authId={authId} handleAvatar={this.handleAvatar} />
<TextField
autoFocus
margin='dense'
@@ -181,7 +186,13 @@ export class ManageTeamEdit extends Component {
}
}
handleAvatar (avatarUrl) {
console.log('handleAvatar: ', avatarUrl)
this.setState({ avatar: avatarUrl })
}
async handleSubmit (e, updateTeamProfile, slug) {
console.log('handleSubmit: ', this.state)
e.preventDefault()
const currentUser = await getItem('user')
let authId = JSON.parse(currentUser)
@@ -191,7 +202,9 @@ export class ManageTeamEdit extends Component {
slug,
owner: authId,
description: this.state.description,
motto: this.state.motto
motto: this.state.motto,
avatar: this.state.avatar,
banner: this.state.banner
}
})
this.setState({ description: '', motto: '', open: false })
@@ -12,32 +12,41 @@ import ManageTeamDetails from './ManageTeamDetails'
import ManageTeamDelete from './ManageTeamDelete'
import ManageTeamEdit from './ManageTeamEdit'
export const ManageTeamsItem = ({ classes, team, authId }) => (
<Grid item xs={12}>
<Card className={classes.card}>
<div className={classes.cardDetails}>
<CardMedia
className={classes.cardMedia}
image='data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22288%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20288%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_164edaf95ee%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_164edaf95ee%22%3E%3Crect%20width%3D%22288%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%2296.32500076293945%22%20y%3D%22118.8%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E' // eslint-disable-line max-len
title='Image title'
/>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant='headline' component='h2'>
{team.name}
</Typography>
<Typography variant='caption' color='textSecondary'>
{team.createdAt} | Members: {team.members.length}
</Typography>
</CardContent>
<CardActions>
<ManageTeamDetails team={team} />
<ManageTeamEdit slug={team.slug} authId={authId} team={team} />
<ManageTeamDelete slug={team.slug} authId={authId} />
</CardActions>
</div>
</Card>
</Grid>
)
export const ManageTeamsItem = ({ classes, team, authId }) => {
console.log('ManageTeamsItem', team, team.profile, team.profile.avatar)
let avatar
if (team.profile.avatar !== undefined && team.profile.avatar !== 'a') {
avatar = team.profile.avatar
} else {
avatar = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22288%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20288%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_164edaf95ee%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A14pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_164edaf95ee%22%3E%3Crect%20width%3D%22288%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%2296.32500076293945%22%20y%3D%22118.8%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'
}
return (
<Grid item xs={12}>
<Card className={classes.card}>
<div className={classes.cardDetails}>
<CardMedia
className={classes.cardMedia}
image={avatar}
title={team.name}
/>
<CardContent className={classes.cardContent}>
<Typography gutterBottom variant='headline' component='h2'>
{team.name}
</Typography>
<Typography variant='caption' color='textSecondary'>
{team.createdAt} | Members: {team.members.length}
</Typography>
</CardContent>
<CardActions>
<ManageTeamDetails team={team} />
<ManageTeamEdit slug={team.slug} authId={authId} team={team} />
<ManageTeamDelete slug={team.slug} authId={authId} />
</CardActions>
</div>
</Card>
</Grid>
)
}
ManageTeamsItem.propTypes = {
classes: PropTypes.object.isRequired,