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

Unprofile: add avatar image upload #37

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/explorer/src/views/Browse.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,20 @@ const Page = () => {
const created = Date.now() - meta.created
return (
<li key={meta.id} className='text-gray-700 py-4'>
<p><small>{humanize(created)}</small></p>
<p><small>{profile.memberId}</small></p>
<p><b>{profile.name}</b> ({profile.pronouns})</p>

{profile.avatar &&
<div className='my-2 mx-auto grid place-items-center box-border rounded-full h-32 w-32'>
<img
src={profile.avatar} alt='avatar'
className='w-full h-full object-cover rounded-full'
/>
</div>}
<p>{profile.city} - {profile.company}</p>
<p>{profile.bio}</p>
<p>{profile.email} - {profile.twitter}</p>
<p><small>{profile.memberId}</small></p>
<p><small>{humanize(created)}</small></p>
</li>
)
})}
Expand Down
1 change: 1 addition & 0 deletions packages/profile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"ethers": "5.6.8",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-image-file-resizer": "^0.4.8",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This library doesn't add enough value to warrant adding it IMHO.

"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"sass": "^1.50.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/profile/src/index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
@tailwind base;

@layer base {
a {
@apply text-kernel-eggplant-light
}
}
@tailwind components;
@tailwind utilities;
111 changes: 109 additions & 2 deletions packages/profile/src/views/Profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@

import { useEffect, useReducer } from 'react'
import { useNavigate } from 'react-router-dom'
import Resizer from 'react-image-file-resizer'
import { Switch } from '@headlessui/react'
import { useServices, Navbar, Footer, Alert } from '@kernel/common'
import { useServices, Navbar, Footer, Alert, getUrl } from '@kernel/common'

import AppConfig from 'App.config'

const AVATAR_CONFIG = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this ensure the size stays below a limit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean the file size, it doesn't, and neither would a straightforward canvas implementation. I figure there is an upper bound to how large of a file a 500x500 image can be, and resizing it by height/width would keep it reasonably small in the vast majority of cases (or maybe in all cases if there's not some way that a 500x500 image can be huge). Since you didn't seem to be attached to a specific hard limit, it didn't feel necessary to sink more effort into enforcing both a max height/width and a max file size.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File size is my primary concern and it should definitely be checked.

maxHeight: 500,
maxWidth: 500,
format: 'PNG',
quality: 100, // maximum: no compression
rotation: 0,
outputType: 'base64'
}

const FORM_INPUT = ['email', 'name', 'pronouns', 'twitter', 'city', 'company', 'bio']

// initializes state in the form:
Expand All @@ -21,6 +31,7 @@ const INITIAL_FORM_KEYS = ['wallet'].concat(FORM_INPUT)
const INITIAL_FORM_FIELDS_STATE = INITIAL_FORM_KEYS
.reduce((acc, key) => Object.assign(acc, { [key]: '' }), {})
INITIAL_FORM_FIELDS_STATE.consent = true
INITIAL_FORM_FIELDS_STATE.avatar = null

const INITIAL_FORM_SUBMISSION_STATE = {
formStatus: 'clean',
Expand Down Expand Up @@ -56,13 +67,46 @@ const reducer = (state, action) => {
const change = (dispatch, type, e) => {
try {
const target = e.target

if (type === 'avatar') {
onAvatarChange(dispatch, target)
target.value = null // reset so onChange will fire even for same image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the dom cannot directly be manipulated in react.

I recommend you read up on controlled vs uncontrolled components:
https://reactjs.org/docs/uncontrolled-components.html

return
}

const payload = target.type === 'checkbox' ? target.checked : target.value
dispatch({ type, payload })
} catch (error) {
console.log(error)
}
}

const onAvatarChange = (dispatch, target) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you should define fns before they are called ex. this is called in change and then defined later. It should be switched.

if (target.files.length === 0) {
return
}

const { maxHeight, maxWidth, format, quality, rotation, outputType } = AVATAR_CONFIG

Resizer.imageFileResizer(
target.files[0],
maxWidth,
maxHeight,
format,
quality,
rotation,
(uri) => {
console.log(uri)
dispatch({ type: 'avatar', payload: uri })
},
outputType
)
}

const removeAvatar = (dispatch) => {
dispatch({ type: 'avatar', payload: null })
}

const value = (state, type) => state[type]

const save = async (state, dispatch, e) => {
Expand Down Expand Up @@ -127,7 +171,12 @@ const ProfileAlert = ({ formStatus, errorMessage }) => {
case 'submitting':
return <Alert type='transparent'>Saving your changes...</Alert>
case 'success':
return <Alert type='success'>Your changes have been saved!</Alert>
return (
<Alert type='success'>
Your changes have been saved!&nbsp;
<a href={getUrl('explorer')}>Start exploring.</a>
</Alert>
)
case 'error':
return <Alert type='danger'>Something went wrong. {errorMessage}</Alert>
default:
Expand Down Expand Up @@ -207,6 +256,64 @@ const Profile = () => {
<Input key={fieldName} fieldName={fieldName} state={state} dispatch={dispatch} />
)
})}
<div className='mt-8 mb-2 w-min'>
<label className='label block mb-1'>
<span className='label-text text-gray-700 capitalize'>Avatar</span>
</label>

<div className='mb-2 text-sm text-gray-700'>
Upload a JPEG or PNG. Recommended size is 500 x 500.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why jpeg and png?

Copy link
Contributor Author

@rorysaur rorysaur Jun 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are just the ones I know should render properly for everyone.

</div>

<div className={`my-8 grid place-items-center box-border rounded-full h-80 w-80
${state.avatar ? '' : 'border-dashed border-2 border-gray-400'}`}
>
{state.avatar
? <img
src={state.avatar} alt='avatar'
className='w-80 h-80 object-cover rounded-full'
/>
: <span className='text-gray-400'>add an image</span>}
</div>

<div className='my-4'>
{state.avatar &&
<div className='grid grid-cols-2 gap-x-2'>
<label
className='px-6 py-3 w-full bg-kernel-eggplant-mid text-white rounded text-center cursor-pointer'
>
<input
type='file' accept='image/png, image/jpeg'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use image/* so mobile phones allow you to take a picture with the camera

see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers

className='hidden'
onChange={change.bind(null, dispatch, 'avatar')}
/>
<span className='inline-block mt-0.5'>Change</span>
</label>
<button
className='px-6 py-3 w-full border-2 border-kernel-eggplant-mid text-kernel-eggplant-mid rounded'
onClick={removeAvatar.bind(null, dispatch)}
>
Remove
</button>
</div>}
{!state.avatar &&
<div className='grid grid-cols-1'>
<label
className='px-6 py-3 w-full bg-kernel-eggplant-mid text-white rounded text-center cursor-pointer'
>
<input
type='file' accept='image/png, image/jpeg'
className='hidden'
onChange={change.bind(null, dispatch, 'avatar')}
/>
<span className='inline-block mt-0.5'>Choose image</span>
</label>
</div>}
</div>
</div>

<hr className='my-12' />

<div className='mt-8 mb-2'>
<Switch.Group>
<Switch
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11117,6 +11117,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==

react-image-file-resizer@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/react-image-file-resizer/-/react-image-file-resizer-0.4.8.tgz#85f4ae4469fd2867d961568af660ef403d7a79af"
integrity sha512-Ue7CfKnSlsfJ//SKzxNMz8avDgDSpWQDOnTKOp/GNRFJv4dO9L5YGHNEnj40peWkXXAK2OK0eRIoXhOYpUzUTQ==

react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Expand Down