Skip to content

Commit

Permalink
Romanchak/2599 image carousel on workshop images (#2611)
Browse files Browse the repository at this point in the history
* updated cropper and change some fields

* Changed function for show message bar about wrong image

* Added new method for optimization checking size of image

* Add carousel package

* Implement image carousel

* Add nav buttons

* Fix files by linter

* Fix uploading/removing images

* Fix files by linter

* Add tests for image-cropper-modal

* Add tests for create-about-form

* Add tests for create-description-form

* Add tests for create-info-form

* Add tests for create-photo-form

* Add tests for teacher-form

* Add tests for create-provider

* Add tests for result

* Add tests for image-form-control

* Add tests for create-provider

* Add tests for image-cropper-modal

* Reimplemented tests for image-cropper-modal

* Add ua and en version of text

* Add test for image-form-control

* Fix i18

* Remove !important

* Remove extra checking

* Remove console.log

* Add translation

* Rework fileChangeEvent

* Create enum for cropper formats

* Create config file for carousel options

* Control the removal of images internally in the images form control

* Implement writeValue function

* Fix tests

* Remove redundant functionality

* Rework image-form-control component

* Fix test

* Implemented advises from Sonar

* Resolved requested changes

---------

Co-authored-by: Yaroslav Petryshyn <ya6liszl@gmail.com>
Co-authored-by: Yaroslav Petryshyn <100092934+ya6lis@users.noreply.github.com>
Co-authored-by: witolDark <witalikspelina@gmail.com>
  • Loading branch information
4 people authored Dec 12, 2024
1 parent 93e7b48 commit 3f538a7
Show file tree
Hide file tree
Showing 37 changed files with 586 additions and 150 deletions.
6 changes: 5 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@
"src/favicon.ico",
"src/assets"
],
"styles": ["./node_modules/leaflet/dist/leaflet.css", "src/styles.scss"],
"styles": [
"./node_modules/leaflet/dist/leaflet.css",
"./node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.carousel.min.css",
"./node_modules/ngx-owl-carousel-o/lib/styles/prebuilt-themes/owl.theme.default.min.css",
"src/styles.scss"],
"scripts": [],
"vendorChunk": true,
"extractLicenses": false,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@
"leaflet": "^1.9.4",
"libphonenumber-js": "^1.10.51",
"moment": "^2.30.1",
"ngx-image-cropper": "7.2.1",
"ngx-image-cropper": "8.0.0",
"ngx-mat-intl-tel-input": "^5.0.0",
"ngx-mat-timepicker": "^16.2.0",
"ngx-owl-carousel-o": "16.0.0",
"ngx-pagination": "^6.0.3",
"rxjs": "~7.8.0",
"tslib": "^2.6.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<div class="image-container">
<img class="image" [src]="images[0]?.path" alt="Image of workshop or provider" height="300" appCustomCarousel />
</div>
<owl-carousel-o [options]="customOptions">
<ng-template carouselSlide *ngFor="let image of images">
<img class="image" [src]="image.path" alt="{{ 'ALT.IMAGE_WORKSHOP_PROVIDER' | translate }}" />
</ng-template>
</owl-carousel-o>
Original file line number Diff line number Diff line change
@@ -1,8 +1,60 @@
.image-container {
height: 300px;
$image-height: 400px;
$dots-height: 24px;
$icon-size: 24px;
$button-size: 50px;
$button-bg-color-hover: #3849f9;
$button-padding: 50px;

.image {
object-fit: contain;
width: 100%;
.image {
object-fit: contain;
width: 100%;
height: $image-height;
position: relative;
}

:host ::ng-deep .owl-carousel {
position: relative;

.owl-prev,
.owl-next {
position: absolute;
top: calc(50% - $dots-height);
width: $button-size;
height: $button-size;
transform: translateY(-50%);
padding: 0;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.1s;

&:hover {
background-color: $button-bg-color-hover;
}
}

.owl-prev {
left: 0;
margin-left: $button-padding;

span {
position: absolute;
left: calc($button-size / 2 - $icon-size / 3);
}
}

.owl-next {
right: 0;
margin-right: $button-padding;
}

.owl-dots {
.owl-dot {
&:hover span,
&.active span {
background-color: $button-bg-color-hover;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';

import { OwlOptions } from 'ngx-owl-carousel-o';
import { ImgPath } from 'shared/models/carousel.model';
import { DefaultCarouselOptions } from 'shared/configs/carousel.config';

@Component({
selector: 'app-image-carousel',
templateUrl: './image-carousel.component.html',
styleUrls: ['./image-carousel.component.scss']
})
export class ImageCarouselComponent {
export class ImageCarouselComponent implements OnInit {
@Input() public images: ImgPath[] = [];

protected customOptions: OwlOptions = { ...DefaultCarouselOptions };

public ngOnInit(): void {
if (this.images.length <= 1) {
this.customOptions = {
...this.customOptions,
loop: false,
autoplay: false,
nav: false,
dots: false
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,17 @@
[cropperMaxHeight]="data.cropperConfig.cropperMaxHeight"
[aspectRatio]="data.cropperConfig.cropperAspectRatio"
[resizeToHeight]="data.cropperConfig.croppedHeight"
format="data.cropperConfig.croppedFormat"
imageQuality="data.cropperConfig.croppedQuality"
[format]="data.cropperConfig.croppedFormat"
[imageQuality]="data.cropperConfig.croppedQuality"
(imageCropped)="imageCropped($event)"
(imageLoaded)="imageLoaded($event)"
(cropperReady)="cropperReady()"
(loadImageFailed)="loadImageFailed()"></image-cropper>
<div *ngIf="invalidMinRequirements">
<small class="warning"
>Зображення недостатнього розміру.<br />Мінімальні розміри: ширина {{ data.cropperConfig.cropperMinWidth }}px, висота
{{ data.cropperConfig.cropperMinHeight }}px.</small
>
</div>
</div>
<div class="flex-[1_1_40%] max-w-[40%]">
<img [src]="croppedImage" class="cropped-image" />
</div>
</div>
<mat-dialog-actions class="btn-wrapper flex flex-row flex-wrap justify-center items-center">
<button class="btn" mat-button cdkFocusInitial (click)="onConfirm()" [disabled]="invalidMinRequirements">ПІДТВЕРДИТИ</button>
<button class="btn btn-cancel" mat-button [mat-dialog-close]="false">СКАСУВАТИ</button>
<button class="btn" mat-button cdkFocusInitial (click)="onConfirm()">{{ 'BUTTONS.CONFIRM' | translate }}</button>
<button class="btn btn-cancel" mat-button [mat-dialog-close]="false">{{ 'BUTTONS.CANCEL' | translate }}</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -1,34 +1,85 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ImageCropperModule } from 'ngx-image-cropper';
import { ImageCroppedEvent, LoadedImage } from 'ngx-image-cropper';
import { NgxsModule, Store } from '@ngxs/store';
import { TranslateModule } from '@ngx-translate/core';

import { ShowMessageBar } from 'shared/store/app.actions';
import { SnackbarText } from 'shared/enum/enumUA/message-bar';
import { Cropper } from '../../models/cropper';
import { ImageCropperModalComponent } from './image-cropper-modal.component';

describe('ImageCropperModalComponent', () => {
let component: ImageCropperModalComponent;
let fixture: ComponentFixture<ImageCropperModalComponent>;
let mockDialogRef: MatDialogRef<ImageCropperModalComponent>;
let store: Store;

const testImage = new Event('test');
const testCropperConfig = { cropperAspectRatio: 1, cropperMinWidth: 100, cropperMinHeight: 100 } as Cropper;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ImageCropperModule, MatDialogModule],
imports: [MatDialogModule, NgxsModule.forRoot(), TranslateModule.forRoot()],
declarations: [ImageCropperModalComponent],
providers: [
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: MatDialogRef, useValue: {} }
{ provide: MAT_DIALOG_DATA, useValue: { image: testImage, cropperConfig: testCropperConfig } },
{ provide: MatDialogRef, useValue: { close: jest.fn() } },
{ provide: Store, useValue: { dispatch: jest.fn() } }
]
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(ImageCropperModalComponent);
component = fixture.componentInstance;
component.data.cropperConfig = {} as Cropper;
component.data.cropperConfig.cropperAspectRatio = 1;

mockDialogRef = TestBed.inject(MatDialogRef);
store = TestBed.inject(Store);

component.data.cropperConfig = testCropperConfig;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should initialize with provided MAT_DIALOG_DATA', () => {
expect(component.data.image).toBe(testImage);
expect(component.data.cropperConfig).toEqual(testCropperConfig);
});

it('should call dialogRef.close with the imageFile when onConfirm is called', () => {
const testImageFile = new File(['test'], 'test-image.png');
component.imageFile = testImageFile;

component.onConfirm();

expect(mockDialogRef.close).toHaveBeenCalledWith(testImageFile);
});

it('should set croppedImage and imageFile when imageCropped is called', () => {
const mockEvent: ImageCroppedEvent = {
base64: null,
blob: new Blob(['test image'], { type: 'image/png' }),
objectUrl: 'http://test.com/test.png',
width: 800,
height: 600,
cropperPosition: { x1: 0, y1: 0, x2: 0, y2: 0 },
imagePosition: { x1: 0, y1: 0, x2: 0, y2: 0 }
};

component.imageCropped(mockEvent);

expect(component.croppedImage).toBe(mockEvent.objectUrl);
expect(component.imageFile).toEqual(mockEvent.blob);
});

it('should dispatch ShowMessageBar action with error message when loadImageFailed is called', () => {
component.loadImageFailed();

expect(store.dispatch).toHaveBeenCalledWith(new ShowMessageBar({ message: SnackbarText.errorToLoadImg, type: 'error' }));
});
});
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ImageCroppedEvent, LoadedImage, base64ToFile } from 'ngx-image-cropper';
import { ImageCroppedEvent } from 'ngx-image-cropper';
import { Store } from '@ngxs/store';

import { Cropper } from 'shared/models/cropper';
import { ShowMessageBar } from 'shared/store/app.actions';
import { SnackbarText } from 'shared/enum/enumUA/message-bar';

@Component({
selector: 'app-image-cropper-modal',
templateUrl: './image-cropper-modal.component.html',
styleUrls: ['./image-cropper-modal.component.scss']
})
export class ImageCropperModalComponent {
public imageChangedEvent = '';
public croppedImage = '';
public imageFile: Blob;
public invalidMinRequirements = false;

constructor(
@Inject(MAT_DIALOG_DATA)
public data: {
image: string;
image: Event;
cropperConfig: Cropper;
},
public dialogRef: MatDialogRef<ImageCropperModalComponent>
public dialogRef: MatDialogRef<ImageCropperModalComponent>,
private readonly store: Store
) {}

public onConfirm(): void {
this.dialogRef.close(this.imageFile);
}

public fileChangeEvent(event: string): void {
this.imageChangedEvent = event;
}

public imageCropped(event: ImageCroppedEvent): void {
this.imageFile = base64ToFile(event.base64);
this.croppedImage = event.base64;
this.croppedImage = event.objectUrl;
this.imageFile = event.blob;
}

public imageLoaded(image: LoadedImage): void {
const { height, width } = image.original.size;
this.invalidMinRequirements = height < this.data.cropperConfig.cropperMinHeight || width < this.data.cropperConfig.cropperMinWidth;
public loadImageFailed(): void {
this.store.dispatch(new ShowMessageBar({ message: SnackbarText.errorToLoadImg, type: 'error' }));
}

public loadImageFailed(): void {}
public cropperReady(): void {}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<form>
<div class="form-wrapper">
<div class="step-label">{{ label }}</div>
<div class="photo-subheader">Додайте фото натистувши на “+”. Максимум {{ imgMaxAmount }} фото</div>
<div class="photo-subheader">{{ 'BANNERS.ADD_IMG_SUBHEADER' | translate }} {{ imgMaxAmount }}</div>
<mat-grid-list [cols]="gridCols" rowHeight="58px" class="logo" gutterSize="8px" (window:resize)="onResize($event.target)">
<mat-grid-tile class="logo_button" *ngIf="decodedImages.length < imgMaxAmount">
<label>
Expand Down
Loading

0 comments on commit 3f538a7

Please sign in to comment.