diff --git a/.angular-cli.json b/.angular-cli.json index 14e9f0c..dc9b898 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -23,6 +23,8 @@ "styles.css", "../node_modules/font-awesome/css/font-awesome.css", "../node_modules/froala-editor/css/froala_editor.pkgd.min.css", + "../node_modules/cropperjs/dist/cropper.css", + "../node_modules/cropperjs/dist/cropper.js", "lighthouse.theme.scss" ], "scripts": [ diff --git a/package-lock.json b/package-lock.json index 05ca3e9..5627362 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "angular-blog", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -323,9 +323,9 @@ } }, "@types/jasmine": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.0.tgz", - "integrity": "sha512-NthRcmfDjp6PEevahmd3L5euekA/H2F7Xb+Mr9C5a8VYYA86pJIEMPqCDVUrJtVyFMY4Dv9XXnUYoSx9Nnxghg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.8.2.tgz", + "integrity": "sha512-RabEJPjYMpjWqW1qYj4k0rlgP5uzyguoc0yxedJdq7t5h19MYvqhjCR1evM3raZ/peHRxp1Qfl24iawvkibSug==", "dev": true }, "@types/jquery": { @@ -1967,6 +1967,11 @@ "sha.js": "2.4.9" } }, + "cropperjs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.1.3.tgz", + "integrity": "sha512-bRdddd35KoPQiTJEX/Pv4mYe6YnNvg4fNsBOZSeBxXt4L3RFhUSfhinc95P6AsbzrHf+ucNftqRF4449Rra6Vw==" + }, "cross-spawn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", @@ -3291,6 +3296,11 @@ "schema-utils": "0.3.0" } }, + "file-type": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-7.2.0.tgz", + "integrity": "sha1-ETz+1S4daVmrgCSJBuLyWozcy3Q=" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", diff --git a/package.json b/package.json index 8e5eb44..30fa2fe 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "@angular/router": "^5.0.0", "angular2-froala-wysiwyg": "^2.6.0", "core-js": "^2.5.1", + "cropperjs": "^1.1.3", + "file-type": "^7.2.0", "hammerjs": "^2.0.8", "rxjs": "^5.5.1", "zone.js": "^0.8.18" diff --git a/src/app/_models/Image.ts b/src/app/_models/Image.ts new file mode 100644 index 0000000..6820b48 --- /dev/null +++ b/src/app/_models/Image.ts @@ -0,0 +1,4 @@ +export class Image { + src: any; + aspectRatio: number; +} diff --git a/src/app/_services/author.service.ts b/src/app/_services/author.service.ts index abee86b..b26281c 100644 --- a/src/app/_services/author.service.ts +++ b/src/app/_services/author.service.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/map'; -import 'rxjs/add/observable/forkJoin'; +import 'rxjs/add/observable/zip'; import { AuthenticationService } from '../_services/authentication.service'; import { Article } from '../_models/Article'; @@ -50,12 +50,13 @@ export class AuthorService { } if (profilePicture) { - return Observable.forkJoin( + return Observable.zip( this.http.put(this.authorUrl + username, body), - this.http.post(this.authorUrl + username, profilePicture) + this.http.post(this.authorUrl + username, profilePicture), + (r1, r2) => r2 ); } else { - return Observable.forkJoin( + return Observable.zip( this.http.put(this.authorUrl + username, body) ); } diff --git a/src/app/article-portal/editor/editor.component.ts b/src/app/article-portal/editor/editor.component.ts index 86e6e8c..1086380 100644 --- a/src/app/article-portal/editor/editor.component.ts +++ b/src/app/article-portal/editor/editor.component.ts @@ -43,7 +43,6 @@ export class EditorComponent implements OnInit { const src = $img.attr('src'); this.imagesService.deleteImage(src) .subscribe(result => { - }) } }, @@ -134,35 +133,42 @@ export class EditorComponent implements OnInit { const coverPhoto = formValue['coverPhoto']; const tags = Array.from(this.selectedTags); - if (coverPhoto.target) { + if (coverPhoto) { const formData = new FormData(); - const file = coverPhoto.target.files[0]; + const file = this.getCoverPhoto(coverPhoto); formData.append('coverPhoto', file); this.editorService.saveArticle(this.content, articleTitle, articleDescription, tags, formData) - .subscribe(result => { - this.snackbarMessageService.displayError('Successfully saved article', 4000); - }, error => { - this.snackbarMessageService.displayError('There was an error while attempting to save this article', 4000); - }); + .subscribe(result => { + this.snackbarMessageService.displayError('Successfully saved article', 4000); + }, error => { + this.snackbarMessageService.displayError('There was an error while attempting to save this article', 4000); + }); } else { this.editorService.saveArticle(this.content, articleTitle, articleDescription, tags) - .subscribe(result => { - this.snackbarMessageService.displayError('Successfully saved article', 4000); - }, error => { - this.snackbarMessageService.displayError('There was an error while attempting to save this article', 4000); - }); + .subscribe(result => { + this.snackbarMessageService.displayError('Successfully saved article', 4000); + }, error => { + this.snackbarMessageService.displayError('There was an error while attempting to save this article', 4000); + }); } } } + getCoverPhoto(coverPhoto: any) { + if (coverPhoto.target) { + return coverPhoto.target.files[0]; + } + return coverPhoto; + } + publishArticle() { this.editorService.publishArticle() - .subscribe(result => { - this.snackbarMessageService.displayError('Successfully published article', 4000); - }, error => { - this.snackbarMessageService.displayError('There was an error while attempting to publish this article', 4000); - }); + .subscribe(result => { + this.snackbarMessageService.displayError('Successfully published article', 4000); + }, error => { + this.snackbarMessageService.displayError('There was an error while attempting to publish this article', 4000); + }); } filterTags(text: string): Observable { @@ -202,6 +208,7 @@ export class EditorComponent implements OnInit { } fileChangeListener($event) { + const image = new Image(); const file = $event.target.files[0]; const myReader = new FileReader(); myReader.onloadend = (loadEvent: any) => { @@ -213,10 +220,18 @@ export class EditorComponent implements OnInit { openPreview() { const dialogRef = this.dialog.open(ImagePreviewComponent, { - data: this.image + data: { + src: this.image, + aspectRatio: 16 / 9 + } }); dialogRef.afterClosed().subscribe(result => { + if (result) { + this.formGroup.patchValue({ + coverPhoto: result + }); + } }); } diff --git a/src/app/article-portal/image-preview/image-preview.component.html b/src/app/article-portal/image-preview/image-preview.component.html index 53249c2..f578451 100644 --- a/src/app/article-portal/image-preview/image-preview.component.html +++ b/src/app/article-portal/image-preview/image-preview.component.html @@ -1 +1,25 @@ - \ No newline at end of file +
+
+ + + + +
+ +
+ + +
+
\ No newline at end of file diff --git a/src/app/article-portal/image-preview/image-preview.component.scss b/src/app/article-portal/image-preview/image-preview.component.scss new file mode 100644 index 0000000..e3b3545 --- /dev/null +++ b/src/app/article-portal/image-preview/image-preview.component.scss @@ -0,0 +1,17 @@ +#image { + max-width: 100%; +} + +.right { + float: right; +} + +.options-row { + display: flex; + align-items: flex-end; + justify-content: center; +} + +.end { + align-self: flex-end; +} \ No newline at end of file diff --git a/src/app/article-portal/image-preview/image-preview.component.ts b/src/app/article-portal/image-preview/image-preview.component.ts index be5c832..c6a0a64 100644 --- a/src/app/article-portal/image-preview/image-preview.component.ts +++ b/src/app/article-portal/image-preview/image-preview.component.ts @@ -1,19 +1,94 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnInit, OnDestroy } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import Cropper from 'cropperjs' +import { SnackbarMessagingService } from 'app/_services/snackbar-messaging.service'; +import { Image } from 'app/_models/Image'; @Component({ selector: 'app-image-preview', templateUrl: './image-preview.component.html', + styleUrls: ['./image-preview.component.scss'] }) -export class ImagePreviewComponent { +export class ImagePreviewComponent implements OnInit, OnDestroy { + + private cropper: Cropper; + private originalImage: any; + private imageBlob: any; + private cropped: boolean; + private croppedCanvas: any; + private aspectRatio: number = 16 / 9; + + public showingCroppingTools: boolean; constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public image: any + private snackbarMessagingService: SnackbarMessagingService, + @Inject(MAT_DIALOG_DATA) public image: Image ) { + this.originalImage = image.src; + this.aspectRatio = image.aspectRatio; + this.cropped = false; + } + + ngOnInit() { + this.showingCroppingTools = false; + } + + ngOnDestroy() { + this.cropper = null; + } + + showCropperTool() { + const image = document.getElementById('image'); + this.cropper = new Cropper(image, { + aspectRatio: this.aspectRatio + }); + this.showingCroppingTools = true; + } + + cropImage() { + this.cropped = true; + this.croppedCanvas = this.cropper.getCroppedCanvas(); + const imageData = this.croppedCanvas.toDataURL(); + this.cropper.replace(imageData); + } + + save() { + if (this.cropped) { + const image = document.getElementById('image').getAttribute('src'); + const imageData = this.croppedCanvas.toBlob((blob) => { + this.imageBlob = blob; + this.stop(); + document.getElementById('image').setAttribute('src', image); + }); + } else { + this.snackbarMessagingService.displayError('No changes detected', 2000); + } + } + + stop() { + if (this.cropper) { + this.cropper.destroy(); + this.cropper = null; + this.showingCroppingTools = false; + } + } + + restore() { + this.cropper.replace(this.originalImage); + this.cropped = false; + } + + saveImage() { + if (this.cropped) { + this.dialogRef.close(this.imageBlob); + } else { + this.snackbarMessagingService.displayError('No changes detected', 2000); + } } onNoClick(): void { + this.stop(); this.dialogRef.close(); } } diff --git a/src/app/article-portal/settings-modal/settings-modal.component.html b/src/app/article-portal/settings-modal/settings-modal.component.html index 93e7df2..ce554ab 100644 --- a/src/app/article-portal/settings-modal/settings-modal.component.html +++ b/src/app/article-portal/settings-modal/settings-modal.component.html @@ -27,7 +27,7 @@ - + \ No newline at end of file diff --git a/src/app/article-portal/settings-modal/settings-modal.component.ts b/src/app/article-portal/settings-modal/settings-modal.component.ts index d183c57..269d9b4 100644 --- a/src/app/article-portal/settings-modal/settings-modal.component.ts +++ b/src/app/article-portal/settings-modal/settings-modal.component.ts @@ -21,6 +21,7 @@ export class SettingsModalComponent implements OnInit { username: string; public saveInProgress: boolean; public image: any; + private profilePictureUpdated: boolean = false; constructor( fb: FormBuilder, @@ -45,6 +46,7 @@ export class SettingsModalComponent implements OnInit { 'profilePicture': {} }); this.image = author.profilePicture; + this.profilePictureUpdated = false; this.username = author.username; }); this.saveInProgress = false; @@ -56,15 +58,18 @@ export class SettingsModalComponent implements OnInit { const name = formValue['name']; const email = formValue['email']; const profilePicture = formValue['profilePicture']; - if (profilePicture.target) { + + if (profilePicture && this.profilePictureUpdated) { const formData = new FormData(); - const file = profilePicture.target.files[0]; + const file = this.getProfilePicture(profilePicture); formData.append('profilePicture', file); + this.authorService.updateUserSettings(this.username, name, email, formData) .subscribe(result => { + console.log('Results', result); this.saveInProgress = false; this.snackBarMessagingService.displayError('Updated user settings', 4000); - this.dialogRef.close({name, image: result.image || ''}); + this.dialogRef.close({name, image: result.data.profilePicture || ''}); }, error => { this.saveInProgress = false; this.snackBarMessagingService.displayError(`Error updating user settings ${error}`, 4000); @@ -85,11 +90,20 @@ export class SettingsModalComponent implements OnInit { } } + getProfilePicture(profilePicture: any) { + if (profilePicture.target) { + return profilePicture.target.files[0]; + } + return profilePicture; + } + fileChangeListener($event) { + const image = new Image(); const file = $event.target.files[0]; const myReader = new FileReader(); myReader.onloadend = (loadEvent: any) => { - this.image = loadEvent.target.result; + image.src = loadEvent.target.result; + this.profilePictureUpdated = true; }; myReader.readAsDataURL(file); @@ -97,10 +111,21 @@ export class SettingsModalComponent implements OnInit { openPreview() { const dialogRef = this.dialog.open(ImagePreviewComponent, { - data: this.image + maxHeight: '400px', + maxWidth: '400px', + data: { + src: this.image, + aspectRatio: 1 + } }); dialogRef.afterClosed().subscribe(result => { + if (result) { + this.settingsGroup.patchValue({ + profilePicture: result + }); + this.profilePictureUpdated = true; + } }); } diff --git a/src/app/nav-bar/nav-bar.component.ts b/src/app/nav-bar/nav-bar.component.ts index 605d738..6e68549 100755 --- a/src/app/nav-bar/nav-bar.component.ts +++ b/src/app/nav-bar/nav-bar.component.ts @@ -72,9 +72,11 @@ export class NavBarComponent implements OnInit { minWidth: '40vw' }).afterClosed() .subscribe(result => { - if (result) { + if (result.name) { this.name = Promise.resolve(result.name); - this.image = Promise.resolve(result.image || environment.DEFAULT_PROFILE_PICTURE); + } + if (result.image) { + this.image = Promise.resolve(result.image); } }); }