PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.1.7.2
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.1.7.2
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
learnpress / assets / src / apps / js / frontend / profile / avatar.js

avatar.js in LearnPress – WordPress LMS Plugin for Create and Sell Online Courses 4.1.7.2, at assets/src/apps/js/frontend/profile/avatar.js

338 lines 9.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useState, useCallback, useEffect, useRef } from '@wordpress/element';
2 import apiFetch from '@wordpress/api-fetch';
3 import { __, sprintf } from '@wordpress/i18n';
4 import Cropper from 'react-easy-crop';
5
6 export default function Avatar() {
7 const [ file, setFile ] = useState();
8 const [ width, setWidth ] = useState( 0 );
9 const [ height, setHeight ] = useState( 0 );
10 const [ crop, setCrop ] = useState( { x: 0, y: 0 } );
11 const [ rotation, setRotation ] = useState( 0 );
12 const [ croppedAreaPixels, setCroppedAreaPixels ] = useState( null );
13 const [ naturalWidth, setNaturalWidth ] = useState( 0 );
14 const [ naturalHeight, setNaturalHeight ] = useState( 0 );
15 const [ loading, setLoading ] = useState( false );
16 const [ skeleton, setSkeleton ] = useState( true );
17 const [ uploadError, setUploadError ] = useState( '' );
18 const [ notice, setNotice ] = useState( {
19 type: '',
20 message: '',
21 } );
22
23 const fileInput = useRef();
24
25 useEffect( () => {
26 async function getAvatar() {
27 setSkeleton( true );
28 try {
29 const response = await apiFetch( {
30 method: 'GET',
31 path: 'lp/v1/profile/get-avatar',
32 } );
33
34 setWidth( response?.data?.width ? parseInt( response.data.width ) : 0 );
35 setHeight( response?.data?.height ? parseInt( response.data.height ) : 0 );
36 setFile( response?.data?.url ? response.data.url : '' );
37 } catch ( error ) {
38 setNotice( {
39 type: 'error',
40 message: error.message,
41 } );
42 }
43 setSkeleton( false );
44 }
45
46 getAvatar();
47 }, [] );
48
49 function readFile( file ) {
50 return new Promise( ( resolve ) => {
51 const reader = new FileReader();
52 reader.addEventListener( 'load', () => resolve( reader.result ), false );
53 reader.readAsDataURL( file );
54 } );
55 }
56
57 const onCropComplete = useCallback( ( croppedArea, croppedAreaPixels ) => {
58 setCroppedAreaPixels( croppedAreaPixels );
59 }, [] );
60
61 const base64Resize = ( base64, width, height ) => {
62 return new Promise( ( resolve, reject ) => {
63 const canvas = document.createElement( 'canvas' );
64 const img = document.createElement( 'img' );
65 img.src = base64;
66 img.setAttribute( 'crossOrigin', 'anonymous' );
67 img.onload = () => {
68 if ( img.naturalWidth > width || img.naturalHeight > height ) {
69 canvas.width = width;
70 canvas.height = height;
71 const ctx = canvas.getContext( '2d' );
72 ctx.drawImage( img, 0, 0, width, height );
73 resolve( canvas.toDataURL( 'image/jpeg' ) );
74 }
75
76 resolve( base64 );
77 };
78 img.onerror = ( err ) => reject( err );
79 } );
80 };
81
82 const updateAvatar = useCallback( async () => {
83 setLoading( { save: true } );
84
85 try {
86 const croppedImage = await getCroppedImg(
87 file,
88 croppedAreaPixels,
89 rotation,
90 );
91
92 const imageResize = await base64Resize( croppedImage, width, height );
93
94 const response = await apiFetch( {
95 path: 'lp/v1/profile/upload-avatar',
96 method: 'POST',
97 data: { file: imageResize || '' },
98 } );
99
100 const { data, status, message } = await response;
101
102 if ( status === 'success' ) {
103 window.location.reload();
104 }
105
106 setNotice( {
107 type: status,
108 message,
109 } );
110 } catch ( e ) {
111 setNotice( {
112 type: 'error',
113 message: e.message || '',
114 } );
115 }
116
117 setLoading( { save: false } );
118 }, [ croppedAreaPixels, rotation ] );
119
120 const setFileInput = async ( fileUpload ) => {
121 const file = await readFile( fileUpload );
122
123 const img = new Image();
124 img.src = await file;
125 img.onload = await function() {
126 setNaturalWidth( img.naturalWidth );
127 setNaturalHeight( img.naturalHeight );
128
129 let error = '';
130 if ( parseInt( fileUpload.size ) > 2097152 ) {
131 error = __( 'File size too large. You need to upload a file < 2MB', 'learnpress' );
132 } else if ( img.naturalWidth < width || img.naturalHeight < height ) {
133 error = sprintf( __( 'Image size must be greater than or equal to %1$sx%2$spx', 'learnpress' ), width, height );
134 }
135
136 if ( error ) {
137 setUploadError( error );
138 } else {
139 setUploadError( '' );
140 setFile( file );
141 }
142 };
143 };
144
145 async function removeAvatar() {
146 if ( confirm( __( 'Are you sure you want to remove your avatar?', 'learnpress' ) ) ) {
147 setLoading( { remove: true } );
148 try {
149 const response = await apiFetch( {
150 path: 'lp/v1/profile/remove-avatar',
151 method: 'POST',
152 } );
153
154 const { data, status, message } = await response;
155
156 setNotice( {
157 type: status,
158 message,
159 } );
160
161 setFile( '' );
162 } catch ( e ) {
163 setNotice( {
164 type: 'error',
165 message: e.message || '',
166 } );
167 }
168 setLoading( { remove: false } );
169 }
170 }
171
172 return (
173 <div className="learnpress_avatar">
174 { ! skeleton ? (
175 <>
176 { file && ! uploadError && (
177 <>
178 { naturalHeight && naturalWidth ? (
179 <div className="learnpress_avatar__cropper">
180 <div style={ { position: 'relative', width: naturalWidth, height: naturalHeight, zIndex: 9999, maxWidth: '100%', maxHeight: '800px' } }>
181 <Cropper
182 image={ file }
183 crop={ crop }
184 zoom="1"
185 cropSize={ { width, height } }
186 onCropChange={ setCrop }
187 onCropComplete={ onCropComplete }
188 />
189 </div>
190
191 <div>
192 <button className={ `learnpress_avatar__button learnpress_avatar__button--save ${ loading?.save ? 'learnpress_avatar__button--loading' : '' }` } onClick={ updateAvatar }>{ __( 'Save', 'learnpress' ) }</button>
193 </div>
194 </div>
195 ) : (
196 <div className="learnpress_avatar__cropper">
197 <img src={ file } alt="" />
198
199 <div>
200 <button className={ `learnpress_avatar__button learnpress_avatar__button--replace` } onClick={ () => fileInput.current && fileInput.current.click() }>{ __( 'Replace', 'learnpress' ) }</button>
201 <button className={ `learnpress_avatar__button learnpress_avatar__button--remove ${ loading?.remove ? 'learnpress_avatar__button--loading' : '' }` } onClick={ removeAvatar }>{ __( 'Remove', 'learnpress' ) }</button>
202 </div>
203 </div>
204 ) }
205 </>
206 ) }
207
208 <form style={ { display: ! file ? '' : 'none' } }>
209 <div className="learnpress_avatar__form">
210 <div className="learnpress_avatar__form-group">
211 <label htmlFor="avatar-file">
212 <div className="learnpress_avatar__form__upload">
213 <div>
214 <span><svg viewBox="64 64 896 896" focusable="false" data-icon="plus" width="1em" height="1em" fill="currentColor" aria-hidden="true"><defs><style></style></defs><path d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"></path><path d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"></path></svg></span>
215 <div>{ __( 'Upload', 'learnpress' ) }</div>
216 </div>
217 </div>
218 <input ref={ fileInput } type="file" id="avatar-file" accept="image/*" onChange={ ( e ) => setFileInput( e.target.files && e.target.files.length > 0 ? e.target.files[ 0 ] : '' ) } />
219 </label>
220 </div>
221 </div>
222 </form>
223
224 { uploadError && (
225 <div className={ `lp-ajax-message error` } style={ { display: 'block' } }>{ uploadError }</div>
226 ) }
227
228 { ! uploadError && notice && notice.type && notice.message && (
229 <div className={ `lp-ajax-message ${ notice.type }` } style={ { display: 'block' } }>{ notice.message }</div>
230 ) }
231 </>
232 ) : (
233 <ul className="lp-skeleton-animation">
234 <li style={ { width: 200, height: 200 } }></li>
235 <li style={ { width: 200, height: 20 } }></li>
236 <li style={ { width: 200, height: 20 } }></li>
237 </ul>
238 ) }
239 </div>
240 );
241 }
242
243 // Link: https://codesandbox.io/s/q8q1mnr01w
244 const createImage = ( url ) =>
245 new Promise( ( resolve, reject ) => {
246 const image = new Image();
247 image.addEventListener( 'load', () => resolve( image ) );
248 image.addEventListener( 'error', ( error ) => reject( error ) );
249 image.setAttribute( 'crossOrigin', 'anonymous' ); // needed to avoid cross-origin issues on CodeSandbox
250 image.src = url;
251 } );
252
253 function getRadianAngle( degreeValue ) {
254 return ( degreeValue * Math.PI ) / 180;
255 }
256
257 /**
258 * Returns the new bounding area of a rotated rectangle.
259 *
260 * @param width
261 * @param height
262 * @param rotation
263 */
264 function rotateSize( width, height, rotation ) {
265 const rotRad = getRadianAngle( rotation );
266
267 return {
268 width:
269 Math.abs( Math.cos( rotRad ) * width ) + Math.abs( Math.sin( rotRad ) * height ),
270 height:
271 Math.abs( Math.sin( rotRad ) * width ) + Math.abs( Math.cos( rotRad ) * height ),
272 };
273 }
274
275 /**
276 * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
277 *
278 * @param imageSrc
279 * @param pixelCrop
280 * @param rotation
281 * @param flip
282 */
283 async function getCroppedImg(
284 imageSrc,
285 pixelCrop,
286 rotation = 0,
287 flip = { horizontal: false, vertical: false }
288 ) {
289 const image = await createImage( imageSrc );
290 const canvas = document.createElement( 'canvas' );
291 const ctx = canvas.getContext( '2d' );
292
293 if ( ! ctx ) {
294 return null;
295 }
296
297 const rotRad = getRadianAngle( rotation );
298
299 // calculate bounding box of the rotated image
300 const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
301 image.width,
302 image.height,
303 rotation
304 );
305
306 // set canvas size to match the bounding box
307 canvas.width = bBoxWidth;
308 canvas.height = bBoxHeight;
309
310 // translate canvas context to a central location to allow rotating and flipping around the center
311 ctx.translate( bBoxWidth / 2, bBoxHeight / 2 );
312 ctx.rotate( rotRad );
313 ctx.scale( flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1 );
314 ctx.translate( -image.width / 2, -image.height / 2 );
315
316 // draw rotated image
317 ctx.drawImage( image, 0, 0 );
318
319 // croppedAreaPixels values are bounding box relative
320 // extract the cropped image using these values
321 const data = ctx.getImageData(
322 pixelCrop.x,
323 pixelCrop.y,
324 pixelCrop.width,
325 pixelCrop.height
326 );
327
328 // set canvas width to final desired crop size - this will clear existing context
329 canvas.width = pixelCrop.width;
330 canvas.height = pixelCrop.height;
331
332 // paste generated rotate image at the top left corner
333 ctx.putImageData( data, 0, 0 );
334
335 // As Base64 string
336 return canvas.toDataURL( 'image/jpeg' );
337 }
338