From f85577e0a96ed71498bf2019cbaaa55257427b70 Mon Sep 17 00:00:00 2001 From: "stepan.moc" Date: Sun, 21 Apr 2024 15:35:26 +0200 Subject: [PATCH] Feature/42 create fe header (#51) * add weather graph and test. --- frontend/package-lock.json | 26 ++++++++ frontend/package.json | 2 + .../src/app/auth/login/login.component.ts | 13 ++-- .../app/auth/register/register.component.ts | 2 +- .../shared/navigation/navigation.component.ts | 11 ++-- .../weather-detail.component.css | 2 +- .../weather-detail.component.html | 10 +-- .../weather-detail.component.ts | 45 +++++++++----- .../weather-graph/weather-graph.component.css | 0 .../weather-graph.component.html | 3 + .../weather-graph/weather-graph.component.ts | 62 +++++++++++++++++++ frontend/src/test/app.component.spec.ts | 5 +- .../src/test/auth/login.component.spec.ts | 4 +- .../src/test/auth/register.component.spec.ts | 4 +- .../navigation/navigation.component.spec.ts | 14 ++--- .../weather/weather-detail.component.spec.ts | 27 ++++---- .../weather/weather-graph.component.spec.ts | 23 +++++++ 17 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 frontend/src/app/weather/weather-graph/weather-graph.component.css create mode 100644 frontend/src/app/weather/weather-graph/weather-graph.component.html create mode 100644 frontend/src/app/weather/weather-graph/weather-graph.component.ts create mode 100644 frontend/src/test/weather/weather-graph.component.spec.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aeb8e91..cc91e85 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,8 @@ "@angular/platform-browser": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", + "chart.js": "^4.4.2", + "moment": "^2.30.1", "rxjs": "~7.8.0", "sweetalert2": "^11.10.7", "ts-node": "^10.9.2", @@ -3685,6 +3687,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -6787,6 +6794,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.2.tgz", + "integrity": "sha512-6GD7iKwFpP5kbSD4MeRRRlTnQvxfQREy36uEtm1hzHzcOqwWx0YEHuspuoNlslu+nciLIB7fjjsHkUv/FzFcOg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -12996,6 +13014,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index cffb6f5..06699a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,8 @@ "@angular/platform-browser": "^17.3.0", "@angular/platform-browser-dynamic": "^17.3.0", "@angular/router": "^17.3.0", + "chart.js": "^4.4.2", + "moment": "^2.30.1", "rxjs": "~7.8.0", "sweetalert2": "^11.10.7", "ts-node": "^10.9.2", diff --git a/frontend/src/app/auth/login/login.component.ts b/frontend/src/app/auth/login/login.component.ts index 2b29da8..0143168 100644 --- a/frontend/src/app/auth/login/login.component.ts +++ b/frontend/src/app/auth/login/login.component.ts @@ -1,17 +1,16 @@ import {ChangeDetectionStrategy, Component, inject, OnInit} from '@angular/core'; import {AuthService} from "../service/auth.service"; -import {FormBuilder, FormGroup, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators} from "@angular/forms"; +import {FormBuilder, FormGroup, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators} from "@angular/forms"; import {LoginForm} from "../model/LoginForm"; -import {MatFormField, MatFormFieldModule, MatPrefix} from "@angular/material/form-field"; +import {MatFormField, MatPrefix} from "@angular/material/form-field"; import {MatCard} from "@angular/material/card"; import {MatToolbar} from "@angular/material/toolbar"; -import {MatIcon, MatIconModule} from "@angular/material/icon"; -import {MatInput, MatInputModule} from "@angular/material/input"; -import {MatButton, MatButtonModule} from "@angular/material/button"; +import {MatIcon} from "@angular/material/icon"; +import {MatInput} from "@angular/material/input"; +import {MatButton} from "@angular/material/button"; import {NgIf} from "@angular/common"; import {FrontendNotificationService} from "../../shared/frontend-notification/service/frontend-notification.service"; import {Router} from "@angular/router"; -import {AppModule} from "../../app.module"; @Component({ selector: 'app-login', @@ -40,7 +39,7 @@ import {AppModule} from "../../app.module"; ] }) export class LoginComponent implements OnInit { - formGroup!: FormGroup + protected formGroup!: FormGroup private formBuilder = inject(FormBuilder) private authService = inject(AuthService) private notificationService = inject(FrontendNotificationService) diff --git a/frontend/src/app/auth/register/register.component.ts b/frontend/src/app/auth/register/register.component.ts index d86bfff..8cb8717 100644 --- a/frontend/src/app/auth/register/register.component.ts +++ b/frontend/src/app/auth/register/register.component.ts @@ -40,7 +40,7 @@ import {RegistrationForm} from "../model/RegistrationForm"; ] }) export class RegisterComponent implements OnInit { - formGroup!: FormGroup + protected formGroup!: FormGroup private formBuilder = inject(FormBuilder) private authService = inject(AuthService) private notificationService = inject(FrontendNotificationService) diff --git a/frontend/src/app/shared/navigation/navigation.component.ts b/frontend/src/app/shared/navigation/navigation.component.ts index 34fda38..75470f7 100644 --- a/frontend/src/app/shared/navigation/navigation.component.ts +++ b/frontend/src/app/shared/navigation/navigation.component.ts @@ -35,9 +35,10 @@ import {filter, Subscription} from "rxjs"; styleUrl: './navigation.component.css' }) export class NavigationComponent implements OnInit, OnDestroy { - currentUrl: string = '' - isUserSignedIn: WritableSignal = signal(false) - private readonly onChangeSubs: Subscription[] = [] + protected isUserSignedIn: WritableSignal = signal(false) + private readonly subscriptions: Subscription[] = [] + private currentUrl: string = '' + private router = inject(Router) private authService = inject(AuthService) @@ -53,11 +54,11 @@ export class NavigationComponent implements OnInit, OnDestroy { this.currentUrl = (event as NavigationEnd).url this.changeDetectorRef.detectChanges() }); - this.onChangeSubs.push(routerSubscription) + this.subscriptions.push(routerSubscription) } ngOnDestroy(): void { - this.onChangeSubs.forEach((subscription) => subscription.unsubscribe()) + this.subscriptions.forEach((subscription) => subscription.unsubscribe()) } isSelected(navigationUrl: string) { diff --git a/frontend/src/app/weather/weather-detail/weather-detail.component.css b/frontend/src/app/weather/weather-detail/weather-detail.component.css index a8943c0..ab49885 100644 --- a/frontend/src/app/weather/weather-detail/weather-detail.component.css +++ b/frontend/src/app/weather/weather-detail/weather-detail.component.css @@ -27,7 +27,7 @@ form { } .mat-mdc-card { - background: #2c3338; + background: white; } .data-content { diff --git a/frontend/src/app/weather/weather-detail/weather-detail.component.html b/frontend/src/app/weather/weather-detail/weather-detail.component.html index 129537a..840ae87 100644 --- a/frontend/src/app/weather/weather-detail/weather-detail.component.html +++ b/frontend/src/app/weather/weather-detail/weather-detail.component.html @@ -4,10 +4,10 @@ cloud - + - + @@ -31,11 +31,7 @@ Forecast Weather -
-

Čas: {{ currentSignal().time }}

-

Teplota: {{ currentSignal().temperature }}°C

-

Oblačnost: {{ currentSignal().cloudCover }}

-
+
diff --git a/frontend/src/app/weather/weather-detail/weather-detail.component.ts b/frontend/src/app/weather/weather-detail/weather-detail.component.ts index 92d1494..54d8164 100644 --- a/frontend/src/app/weather/weather-detail/weather-detail.component.ts +++ b/frontend/src/app/weather/weather-detail/weather-detail.component.ts @@ -1,4 +1,13 @@ -import {ChangeDetectionStrategy, Component, inject, OnInit, signal, WritableSignal} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnDestroy, + OnInit, + signal, + ViewChild, + WritableSignal +} from '@angular/core'; import {MatFormField, MatPrefix} from "@angular/material/form-field"; import {MatInput} from "@angular/material/input"; import {MatButton} from "@angular/material/button"; @@ -9,8 +18,9 @@ import {NgIf} from "@angular/common"; import {FormControl, ReactiveFormsModule} from "@angular/forms"; import {WeatherService} from "../service/weather.service"; import {CurrentWeatherDetail} from "../model/CurrentWeatherDetail"; -import {ForecastWeatherDetail} from "../model/ForecastWeatherDetal"; import {FrontendNotificationService} from "../../shared/frontend-notification/service/frontend-notification.service"; +import {WeatherGraphComponent} from "../weather-graph/weather-graph.component"; +import {Subscription} from "rxjs"; @Component({ selector: 'app-weather-detail', @@ -27,41 +37,46 @@ import {FrontendNotificationService} from "../../shared/frontend-notification/se ReactiveFormsModule, MatPrefix, MatCardContent, - MatCardTitle + MatCardTitle, + WeatherGraphComponent ], providers: [WeatherService], templateUrl: './weather-detail.component.html', styleUrl: './weather-detail.component.css' }) -export class WeatherDetailComponent implements OnInit { - public cityForm = new FormControl(); - public currentSignal: WritableSignal = signal(CurrentWeatherDetail.createDefault()) - public forecastSignal: WritableSignal = signal(ForecastWeatherDetail.createDefault()) +export class WeatherDetailComponent implements OnInit, OnDestroy { + @ViewChild(WeatherGraphComponent) weatherGraphComponent!: WeatherGraphComponent; + + protected cityFormControl = new FormControl(); + protected currentSignal: WritableSignal = signal(CurrentWeatherDetail.createDefault()) private weatherService = inject(WeatherService); private notificationService = inject(FrontendNotificationService); + private subscriptions: Subscription[] = []; ngOnInit(): void { } - getCurrentWeather() { - this.weatherService.getCurrentWeather(this.cityForm.value).subscribe({ + ngOnDestroy(): void { + this.subscriptions.forEach(subscription => subscription.unsubscribe()); + } + + getWeather() { + this.subscriptions.push(this.weatherService.getCurrentWeather(this.cityFormControl.value).subscribe({ next: (response) => { this.currentSignal.set(response) }, error: (error) => { this.notificationService.errorNotification('Město nenalezeno') } - }) - } + })) - getForecastWeather() { - this.weatherService.getForecastWeather(this.cityForm.value).subscribe({ + this.subscriptions.push(this.weatherService.getForecastWeather(this.cityFormControl.value).subscribe({ next: (response) => { - this.forecastSignal.set(response) + this.weatherGraphComponent.createChart(response) }, error: (error) => { this.notificationService.errorNotification('Město nenalezeno') } - }) + })) } } diff --git a/frontend/src/app/weather/weather-graph/weather-graph.component.css b/frontend/src/app/weather/weather-graph/weather-graph.component.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/weather/weather-graph/weather-graph.component.html b/frontend/src/app/weather/weather-graph/weather-graph.component.html new file mode 100644 index 0000000..149073c --- /dev/null +++ b/frontend/src/app/weather/weather-graph/weather-graph.component.html @@ -0,0 +1,3 @@ +
+ {{ chart }} +
diff --git a/frontend/src/app/weather/weather-graph/weather-graph.component.ts b/frontend/src/app/weather/weather-graph/weather-graph.component.ts new file mode 100644 index 0000000..1ec5dea --- /dev/null +++ b/frontend/src/app/weather/weather-graph/weather-graph.component.ts @@ -0,0 +1,62 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {ForecastWeatherDetail} from "../model/ForecastWeatherDetal"; +import {Chart, registerables} from 'chart.js'; +import moment from "moment"; + +@Component({ + selector: 'app-weather-graph', + standalone: true, + imports: [], + templateUrl: './weather-graph.component.html', + styleUrl: './weather-graph.component.css' +}) +export class WeatherGraphComponent implements OnInit, OnDestroy { + protected chart: any; + + ngOnInit() { + Chart.register(...registerables); + this.createChart(ForecastWeatherDetail.createDefault()); + } + + ngOnDestroy(): void { + } + + createChart(data: ForecastWeatherDetail) { + if (this.chart != undefined) { + this.chart.destroy(); + } + this.chart = new Chart("MyChart", { + type: "line", //this denotes tha type of chart + + data: {// values on X-Axis + labels: data.time.map(time => moment(time).format("DD/MM")), + datasets: [ + { + label: "Min Temp", + data: data.minTemperature, + backgroundColor: 'blue' + }, + { + label: "Max Temp", + data: data.maxTemperature, + backgroundColor: 'red' + }, + { + label: "Max wind", + data: data.maxWindSpeed, + backgroundColor: 'grey' + } + ] + }, + options: { + scales: { + y: { + beginAtZero: true + } + }, + responsive: true, + aspectRatio: 1 + } + }); + } +} diff --git a/frontend/src/test/app.component.spec.ts b/frontend/src/test/app.component.spec.ts index 01acfb2..691dad2 100644 --- a/frontend/src/test/app.component.spec.ts +++ b/frontend/src/test/app.component.spec.ts @@ -1,12 +1,11 @@ import {TestBed} from '@angular/core/testing'; import {AppComponent} from "../app/app.component"; -import {HttpClientModule} from "@angular/common/http"; -import {RouterTestingModule} from "@angular/router/testing"; +import {HttpClientTestingModule} from "@angular/common/http/testing"; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ declarations: [AppComponent], - imports: [RouterTestingModule, HttpClientModule], + imports: [HttpClientTestingModule], })); it('should create the app', () => { diff --git a/frontend/src/test/auth/login.component.spec.ts b/frontend/src/test/auth/login.component.spec.ts index e791e55..d832b39 100644 --- a/frontend/src/test/auth/login.component.spec.ts +++ b/frontend/src/test/auth/login.component.spec.ts @@ -44,7 +44,7 @@ describe('LoginComponent', () => { describe('login', () => { it('should login', () => { const validFormValue = {email: "test@test.cz", password: "password"} as LoginForm - component.formGroup = formBuilder.group(validFormValue) + component['formGroup'] = formBuilder.group(validFormValue) fixture.detectChanges() authService.login = jest.fn().mockReturnValue(of({})); @@ -62,7 +62,7 @@ describe('LoginComponent', () => { it('invalid password', () => { const validFormValue = {email: "test@test.cz", password: "password"} as LoginForm - component.formGroup = formBuilder.group(validFormValue) + component['formGroup'] = formBuilder.group(validFormValue) fixture.detectChanges() const notificationSpy = jest.spyOn(notificationService, 'errorNotification'); diff --git a/frontend/src/test/auth/register.component.spec.ts b/frontend/src/test/auth/register.component.spec.ts index e701a6e..45b7eae 100644 --- a/frontend/src/test/auth/register.component.spec.ts +++ b/frontend/src/test/auth/register.component.spec.ts @@ -50,7 +50,7 @@ describe('RegisterComponent', () => { password: "password", passwordConfirmation: "password" } as RegistrationForm - component.formGroup = formBuilder.group(validFormValue) + component['formGroup'] = formBuilder.group(validFormValue) fixture.detectChanges() authService.register = jest.fn().mockReturnValue(of({})); @@ -68,7 +68,7 @@ describe('RegisterComponent', () => { it('should throw error', () => { const validFormValue = {email: "test@test.cz", password: "password"} as LoginForm - component.formGroup = formBuilder.group(validFormValue) + component['formGroup'] = formBuilder.group(validFormValue) fixture.detectChanges() const notificationSpy = jest.spyOn(notificationService, 'errorNotification'); diff --git a/frontend/src/test/shared/navigation/navigation.component.spec.ts b/frontend/src/test/shared/navigation/navigation.component.spec.ts index 44b4e9f..76e4265 100644 --- a/frontend/src/test/shared/navigation/navigation.component.spec.ts +++ b/frontend/src/test/shared/navigation/navigation.component.spec.ts @@ -1,11 +1,11 @@ import {NavigationComponent} from "../../../app/shared/navigation/navigation.component"; import {ComponentFixture, TestBed} from "@angular/core/testing"; import {NavigationEnd, Router} from "@angular/router"; -import {RouterTestingModule} from "@angular/router/testing"; import {HttpService} from "../../../app/shared/http/service/http.service"; import {AuthService} from "../../../app/auth/service/auth.service"; import {HttpClientTestingModule} from "@angular/common/http/testing"; -import {BehaviorSubject, of} from "rxjs"; +import {BehaviorSubject} from "rxjs"; +import {RouterTestingModule} from "@angular/router/testing"; describe('NavigationComponent', () => { let component: NavigationComponent; @@ -33,11 +33,11 @@ describe('NavigationComponent', () => { }); it('should set currentUrl on initialization', () => { - expect(component.currentUrl).toBe('/'); + expect(component['currentUrl']).toBe('/'); }); it('should set isUserSignedIn on initialization', () => { - expect(component.isUserSignedIn()).toBeFalsy(); + expect(component['isUserSignedIn']()).toBeFalsy(); }); it('should update currentUrl on router events', () => { @@ -48,16 +48,16 @@ describe('NavigationComponent', () => { component.ngOnInit(); - expect(component.currentUrl).toBe('/new-url'); + expect(component['currentUrl']).toBe('/new-url'); }); it('should return true when currentUrl matches navigationUrl', () => { - component.currentUrl = '/some-url'; + component['currentUrl'] = '/some-url'; expect(component.isSelected('/some-url')).toBeTruthy(); }); it('should return false when currentUrl does not match navigationUrl', () => { - component.currentUrl = '/some-url'; + component['currentUrl'] = '/some-url'; expect(component.isSelected('/other-url')).toBeFalsy(); }); }); diff --git a/frontend/src/test/weather/weather-detail.component.spec.ts b/frontend/src/test/weather/weather-detail.component.spec.ts index 7d8451f..7147ecd 100644 --- a/frontend/src/test/weather/weather-detail.component.spec.ts +++ b/frontend/src/test/weather/weather-detail.component.spec.ts @@ -39,7 +39,7 @@ describe('WeatherDetailComponent', () => { describe("getCurrentWeather", () => { it("should get current weather", () => { - component.cityForm.setValue("Prague"); + component['cityFormControl'].setValue("Prague"); const response = new CurrentWeatherDetail( new Date(), 20, @@ -51,67 +51,66 @@ describe('WeatherDetailComponent', () => { weatherService.getCurrentWeather = jest.fn().mockReturnValue(of(response)); fixture.ngZone?.run(() => { - component.getCurrentWeather(); + component.getWeather(); }) expect(weatherService.getCurrentWeather).toHaveBeenCalledWith("Prague"); - expect(component.currentSignal()).toEqual(response); + expect(component['currentSignal']()).toEqual(response); }); it("should show error notification when city is not found", () => { const defaultWeather = CurrentWeatherDetail.createDefault(); - component.currentSignal.set(defaultWeather); - component.cityForm.setValue("Prague"); + component['currentSignal'].set(defaultWeather); + component['cityFormControl'].setValue("Prague"); const error = {status: 500} const weatherSpy = weatherService.getCurrentWeather = jest.fn().mockReturnValue(throwError(() => error)); const notificationSpy = jest.spyOn(notificationService, 'errorNotification'); fixture.ngZone?.run(() => { - component.getCurrentWeather(); + component.getWeather(); }) expect(weatherSpy).toHaveBeenCalledWith("Prague"); - expect(component.currentSignal()).toEqual(defaultWeather); + expect(component['currentSignal']()).toEqual(defaultWeather); expect(notificationSpy).toHaveBeenCalledWith('Město nenalezeno'); }); }); describe("getForecastWeather", () => { it("should get forecast weather", () => { - component.cityForm.setValue("Prague"); + component['cityFormControl'].setValue("Prague"); const response = new ForecastWeatherDetail( [new Date()], [20], [50], [50] ); + const createChartSpy = jest.spyOn(component.weatherGraphComponent, 'createChart'); weatherService.getForecastWeather = jest.fn().mockReturnValue(of(response)); fixture.ngZone?.run(() => { - component.getForecastWeather(); + component.getWeather(); }) expect(weatherService.getForecastWeather).toHaveBeenCalledWith("Prague"); - expect(component.forecastSignal()).toEqual(response); + expect(createChartSpy).toHaveBeenCalledWith(response); }); it("should show error notification when city is not found", () => { const defaultWeather = ForecastWeatherDetail.createDefault(); - component.forecastSignal.set(defaultWeather); - component.cityForm.setValue("Prague"); + component['cityFormControl'].setValue("Prague"); const error = {status: 500} const weatherSpy = weatherService.getForecastWeather = jest.fn().mockReturnValue(throwError(() => error)); const notificationSpy = jest.spyOn(notificationService, 'errorNotification'); fixture.ngZone?.run(() => { - component.getForecastWeather(); + component.getWeather(); }) expect(weatherSpy).toHaveBeenCalledWith("Prague"); - expect(component.forecastSignal()).toEqual(defaultWeather); expect(notificationSpy).toHaveBeenCalledWith('Město nenalezeno'); }); }) diff --git a/frontend/src/test/weather/weather-graph.component.spec.ts b/frontend/src/test/weather/weather-graph.component.spec.ts new file mode 100644 index 0000000..3db875a --- /dev/null +++ b/frontend/src/test/weather/weather-graph.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {WeatherGraphComponent} from '../../app/weather/weather-graph/weather-graph.component'; + +describe('WeatherGraphComponent', () => { + let component: WeatherGraphComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [WeatherGraphComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WeatherGraphComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +});