Skip to content

Commit 51a87ea

Browse files
committed
OpenConceptLab/ocl_issues#1617 | user profile | avatar edit/upload
1 parent 20beca1 commit 51a87ea

File tree

7 files changed

+359
-8
lines changed

7 files changed

+359
-8
lines changed

src/components/app/App.scss

+2-5
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,18 @@ div.split-appear {
5858
.user-img-xsmall {
5959
width: 30px;
6060
height: 30px;
61-
border: 1px solid lightgray;
6261
border-radius: 50%;
6362
object-fit: cover;
6463
}
6564
.user-img-small {
6665
width: 45px;
6766
height: 45px;
68-
border: 1px solid lightgray;
6967
border-radius: 50%;
7068
object-fit: cover;
7169
}
7270
.user-img-medium {
73-
width: 80px;
74-
height: 80px;
75-
border: 1px solid lightgray;
71+
width: 100px;
72+
height: 100px;
7673
border-radius: 50%;
7774
object-fit: cover;
7875
}

src/components/common/HeaderLogo.jsx

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import Button from './Button'
4+
import { Dialog, DialogTitle, DialogContent, DialogActions, Tooltip, IconButton } from '@mui/material'
5+
import { Edit as EditIcon, CloudUpload as UploadIcon } from '@mui/icons-material';
6+
import { last } from 'lodash';
7+
import { currentUserHasAccess } from '../../common/utils';
8+
import ImageUploader from './ImageUploader';
9+
import './HeaderLogo.scss';
10+
11+
const HeaderLogo = ({ logoURL, onUpload, defaultIcon, isCircle, shrink, className }) => {
12+
const { t } = useTranslation()
13+
const hasAccess = currentUserHasAccess();
14+
const [base64, setBase64] = React.useState(null);
15+
const [open, setOpen] = React.useState(false);
16+
const onLogoUpload = (base64, name) => {
17+
setOpen(false);
18+
setBase64(base64)
19+
onUpload(base64, name)
20+
}
21+
const getExistingLogoName = () => {
22+
if(!logoURL)
23+
return
24+
25+
return last(logoURL.split('/'))
26+
}
27+
28+
let containerClasses = 'logo-container flex-vertical-center'
29+
if(className)
30+
containerClasses += ` ${className}`
31+
else if(shrink)
32+
containerClasses += ' small'
33+
34+
const logo = base64 || logoURL
35+
36+
return (
37+
<React.Fragment>
38+
<div className={containerClasses}>
39+
{
40+
logo ?
41+
<img className='header-logo' src={logo} /> :
42+
defaultIcon
43+
}
44+
{
45+
hasAccess &&
46+
<Tooltip arrow title={t('user.update_avatar')}>
47+
<IconButton
48+
onClick={() => setOpen(true)}
49+
className='logo-edit-button'
50+
color='secondary'
51+
size="large">
52+
{
53+
logoURL ?
54+
<EditIcon fontSize='small' color='secondary' /> :
55+
<UploadIcon color='secondary' fontSize='small' />
56+
}
57+
</IconButton>
58+
</Tooltip>
59+
}
60+
</div>
61+
<Dialog onClose={() => setOpen(false)} open={open} fullWidth>
62+
<DialogTitle>{t('user.update_avatar')}</DialogTitle>
63+
<DialogContent>
64+
<ImageUploader onUpload={onLogoUpload} defaultImg={logoURL} defaultName={getExistingLogoName()} isCircle={isCircle} />
65+
</DialogContent>
66+
<DialogActions>
67+
<Button onClick={() => setOpen(false)} label={t('common.cancel')} />
68+
</DialogActions>
69+
</Dialog>
70+
</React.Fragment>
71+
);
72+
}
73+
74+
export default HeaderLogo;

src/components/common/HeaderLogo.scss

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
.header-logo {
2+
width: 100px;
3+
display: block;
4+
max-height: 100px;
5+
border-radius: 50%;
6+
}
7+
.logo-container {
8+
justify-content: center;
9+
max-height: 130px;
10+
position: relative;
11+
width:100%;
12+
max-width: 100px;
13+
border-radius: 10px;
14+
padding: 2px;
15+
overflow: hidden;
16+
17+
img {
18+
display:block;
19+
object-fit: cover;
20+
}
21+
.logo-edit-button {
22+
position: absolute;
23+
top: 50%;
24+
left: 50%;
25+
transform: translate(-50%, -50%);
26+
-ms-transform: translate(-50%, -50%);
27+
opacity:0;
28+
border: 1px solid black;
29+
}
30+
&.small {
31+
height: 70px;
32+
img {
33+
height: 70px;
34+
}
35+
}
36+
&.xsmall {
37+
img {
38+
display: block;
39+
width: auto;
40+
height: auto;
41+
max-width: 100px;
42+
max-height: 37px;
43+
}
44+
margin-top: 0px;
45+
height: 36.5px;
46+
svg {
47+
width: 40px;
48+
height: 40px;
49+
}
50+
.logo-edit-button {
51+
height: 30px;
52+
width: 30px;
53+
svg {
54+
width: 20px;
55+
height: 20px;
56+
}
57+
}
58+
}
59+
}
60+
61+
.logo-container:hover .logo-edit-button {
62+
opacity: 1;
63+
}
64+
65+
.logo-container:before {
66+
content:"";
67+
position:absolute;
68+
width:100%;
69+
height:100%;
70+
top:0;left:0;right:0;
71+
background-color:rgba(0,0,0,0);
72+
}
73+
.logo-container:hover {
74+
img {
75+
opacity: 0.5;
76+
}
77+
}
+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import React, { useState, useCallback, useRef, useEffect } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Button, TextField } from '@mui/material';
4+
import { get } from 'lodash';
5+
import ReactCrop from 'react-image-crop';
6+
import "react-image-crop/dist/ReactCrop.css";
7+
8+
// Increase pixel density for crop preview quality on retina screens.
9+
10+
const pixelRatio = window.devicePixelRatio || 1;
11+
12+
// We resize the canvas down when saving on retina devices otherwise the image
13+
// will be double or triple the preview size.
14+
/* const getResizedCanvas = (canvas, newWidth, newHeight) => {
15+
* const tmpCanvas = document.createElement("canvas");
16+
* tmpCanvas.width = newWidth;
17+
* tmpCanvas.height = newHeight;
18+
*
19+
* const ctx = tmpCanvas.getContext("2d");
20+
* ctx.drawImage(
21+
* canvas,
22+
* 0,
23+
* 0,
24+
* canvas.width,
25+
* canvas.height,
26+
* 0,
27+
* 0,
28+
* newWidth,
29+
* newHeight
30+
* );
31+
*
32+
* return tmpCanvas;
33+
* }
34+
* */
35+
/* const generateDownload = (previewCanvas, crop) => {
36+
* if (!crop || !previewCanvas) {
37+
* return;
38+
* }
39+
*
40+
* const canvas = getResizedCanvas(previewCanvas, crop.width, crop.height);
41+
*
42+
* canvas.toBlob(
43+
* (blob) => {
44+
* const previewUrl = window.URL.createObjectURL(blob);
45+
*
46+
* const anchor = document.createElement("a");
47+
* anchor.download = "cropPreview.png";
48+
* anchor.href = URL.createObjectURL(blob);
49+
* anchor.click();
50+
*
51+
* window.URL.revokeObjectURL(previewUrl);
52+
* },
53+
* "image/png",
54+
* 1
55+
* );
56+
* }
57+
* */
58+
59+
const ImageUploader = props => {
60+
const { t } = useTranslation()
61+
const [fileName, setFileName] = useState(props.defaultName);
62+
const [upImg, setUpImg] = useState(props.defaultImg);
63+
const imgRef = useRef(null);
64+
const previewCanvasRef = useRef(null);
65+
const [crop, setCrop] = useState({ unit: "%", width: 30, height: props.isCircle ? undefined : 30, aspect: props.isCircle ? 1 : undefined });
66+
const [completedCrop, setCompletedCrop] = useState(null);
67+
const [base64, setBase64] = useState(null);
68+
69+
const onSelectFile = (e) => {
70+
if (e.target.files && e.target.files.length > 0) {
71+
const reader = new FileReader();
72+
reader.addEventListener("load", () => setUpImg(reader.result));
73+
const file = e.target.files[0];
74+
setFileName(file.name)
75+
reader.readAsDataURL(e.target.files[0]);
76+
}
77+
};
78+
79+
const onLoad = useCallback((img) => {
80+
imgRef.current = img;
81+
}, []);
82+
83+
const uploadImage = () => {
84+
props.onUpload(base64, fileName)
85+
}
86+
87+
useEffect(() => {
88+
if (!completedCrop || !previewCanvasRef.current || !imgRef.current) {
89+
return;
90+
}
91+
92+
const image = imgRef.current;
93+
const canvas = previewCanvasRef.current;
94+
const crop = completedCrop;
95+
96+
const scaleX = image.naturalWidth / image.width;
97+
const scaleY = image.naturalHeight / image.height;
98+
const ctx = canvas.getContext("2d");
99+
100+
canvas.width = crop.width * pixelRatio;
101+
canvas.height = crop.height * pixelRatio;
102+
103+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
104+
ctx.imageSmoothingQuality = "high";
105+
106+
ctx.drawImage(
107+
image,
108+
crop.x * scaleX,
109+
crop.y * scaleY,
110+
crop.width * scaleX,
111+
crop.height * scaleY,
112+
0,
113+
0,
114+
crop.width,
115+
crop.height
116+
);
117+
setBase64(canvas.toDataURL());
118+
}, [completedCrop]);
119+
120+
return (
121+
<div className="App">
122+
<div className='flex-vertical-center'>
123+
<TextField
124+
variant="outlined"
125+
inputProps={{
126+
type: "file",
127+
accept: "image/*"
128+
}}
129+
onChange={onSelectFile}
130+
/>
131+
<Button
132+
style={{marginLeft: '10px'}}
133+
variant='outlined'
134+
color='primary'
135+
disabled={!get(completedCrop, 'width') || !get(completedCrop, 'height')}
136+
onClick={uploadImage}
137+
>
138+
{t('common.upload')}
139+
</Button>
140+
</div>
141+
<div style={{marginTop: '10px'}}>
142+
<ReactCrop
143+
src={upImg}
144+
onImageLoaded={onLoad}
145+
crop={crop}
146+
onChange={(c) => setCrop(c)}
147+
onComplete={(c) => setCompletedCrop(c)}
148+
crossorigin='Anonymous'
149+
circularCrop={props.isCircle}
150+
/>
151+
</div>
152+
<div style={{display: 'none'}}>
153+
<canvas
154+
ref={previewCanvasRef}
155+
style={{
156+
width: Math.round(get(completedCrop, 'width', 0)),
157+
height: Math.round(get(completedCrop, 'height', 0))
158+
}}
159+
/>
160+
</div>
161+
</div>
162+
);
163+
}
164+
165+
export default ImageUploader;

src/components/users/UserEdit.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const UserEdit = () => {
1111
<div className='col-xs-3' style={{height: height, padding: '24px 24px 24px 8px', maxWidth: '20%'}}>
1212
<UserProfile user={user} />
1313
</div>
14-
<div className='col-xs-10 padding-0' style={{height: height, maxWidth: '80%'}}>
14+
<div className='col-xs-10 padding-0' style={{height: height, maxWidth: '80%', overflow: 'auto'}}>
1515
{
1616
user?.url &&
1717
<UserForm user={user} />

0 commit comments

Comments
 (0)