diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f506200..aaa31fd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2921,6 +2921,30 @@ "intersection-observer": "0.7.0" } }, + "@ngrx/effects": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-10.0.1.tgz", + "integrity": "sha512-pw0hRQNlyBBRHH1NRWl3TF+RtEAS4XOSnoTHPtQ84Ib/bEribvexsdEq3k6yLWvR3tLTudb5J6SYwYawcM6omA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-10.0.1.tgz", + "integrity": "sha512-ZbPvhp/tRYnS3jZ28mDOX2LH3jfySXT0uv8ffIboM/o9QxBGHpAJyBct2zkpy4duYBc3i/sIbRn+CEpAjLXjHw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@ngrx/store-devtools": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-10.0.1.tgz", + "integrity": "sha512-kwgF1yjjVn0FER+AG83OLCYSMuX4/E3L+DN4doSoZs4BNO9FdkYIIA4ul1nXT5d6SLiFFTmlufmbgc6HCF3pjQ==", + "requires": { + "tslib": "^2.0.0" + } + }, "@ngtools/webpack": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-10.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 57a76b3..408036e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,9 @@ "@angular/router": "~10.0.0", "@nebular/eva-icons": "5.0.0", "@nebular/theme": "^5.0.0", + "@ngrx/effects": "^10.0.1", + "@ngrx/store": "^10.0.1", + "@ngrx/store-devtools": "^10.0.1", "@types/papaparse": "^5.0.3", "d3": "^5.16.0", "eva-icons": "^1.1.2", diff --git a/frontend/src/app/_services/send-data.service.ts b/frontend/src/app/_services/send-data.service.ts index cc0e3a9..82a38f7 100644 --- a/frontend/src/app/_services/send-data.service.ts +++ b/frontend/src/app/_services/send-data.service.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs'; export class SendDataService { constructor(private http: HttpClient) {} - postFile(file: File): Observable { + postFile(file: File): Observable { const formData: FormData = new FormData(); const requestOptions = { responseType: 'text' as 'json', diff --git a/frontend/src/app/actions/front-page.actions.ts b/frontend/src/app/actions/front-page.actions.ts new file mode 100644 index 0000000..f66a45f --- /dev/null +++ b/frontend/src/app/actions/front-page.actions.ts @@ -0,0 +1,18 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { createAction, props } from '@ngrx/store'; + +export const fetchFile = createAction( + '[FrontPage Component] Fetch File', + props<{ file: File }>() +); +export const sendFile = createAction('[FrontPage Component] Send File'); + +export const sendFileError = createAction( + '[FrontPage Component] Send Error', + props<{ error: HttpErrorResponse }>() +); + +export const sendFileSuccess = createAction( + '[FrontPage Component] Send Success', + props<{ data: string }>() +); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a695b1d..5d1b6d3 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -18,6 +18,10 @@ import { import { FrontPageModule } from './front-page/front-page.module'; import { SharedDataService } from './_services/shared-data.service'; import { SidebarItemsService } from './_services/sidebar-items.service'; +import { StoreModule } from '@ngrx/store'; +import { reducers, metaReducers } from './reducers'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { EffectsModule } from '@ngrx/effects'; @NgModule({ declarations: [AppComponent], @@ -32,6 +36,11 @@ import { SidebarItemsService } from './_services/sidebar-items.service'; NbEvaIconsModule, FrontPageModule, NbToastrModule.forRoot(), + StoreModule.forRoot(reducers, { + metaReducers, + }), + StoreDevtoolsModule.instrument({ maxAge: 25 }), + EffectsModule.forRoot([]), ], bootstrap: [AppComponent], providers: [SharedDataService, NbSidebarService, SidebarItemsService], diff --git a/frontend/src/app/effects/front-page.effects.ts b/frontend/src/app/effects/front-page.effects.ts new file mode 100644 index 0000000..d03d62d --- /dev/null +++ b/frontend/src/app/effects/front-page.effects.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { SendDataService } from '../_services/send-data.service'; +import * as FrontPageActions from '../actions/front-page.actions'; +import * as FrontPageSelectors from '../selectors/front-page.selectors'; +import { Actions, ofType, createEffect } from '@ngrx/effects'; +import { State as AppState } from '../reducers/index'; +import { Store } from '@ngrx/store'; +import { + exhaustMap, + withLatestFrom, + tap, + map, + catchError, + concatMap, + flatMap, +} from 'rxjs/operators'; +import { of } from 'rxjs'; +import { NbToastrService } from '@nebular/theme'; +import { Router } from '@angular/router'; + +@Injectable() +export class FrontPageEffect { + constructor( + private sendDataService: SendDataService, + private actions$: Actions, + private store$: Store, + private toastService: NbToastrService, + private router: Router + ) {} + + file$ = createEffect(() => + this.actions$.pipe( + ofType(FrontPageActions.sendFile), + concatMap((action) => + of(action).pipe( + withLatestFrom(this.store$.select(FrontPageSelectors.selectFile)) + ) + ), + flatMap(([_, file]) => + this.sendDataService.postFile(file).pipe( + map((result) => FrontPageActions.sendFileSuccess({ data: result })), + catchError((error) => of(FrontPageActions.sendFileError({ error }))) + ) + ) + ) + ); + + toast$ = createEffect( + () => + this.actions$.pipe( + ofType(FrontPageActions.sendFileError), + tap(({ error }) => { + if (error.status === 406) { + this.toastService.danger('', 'Format pliku jest niepoprawny!', { + icon: 'alert-circle', + }); + } + }) + ), + { dispatch: false } + ); + + navigate$ = createEffect( + () => + this.actions$.pipe( + ofType(FrontPageActions.sendFileSuccess), + tap(() => this.router.navigate(['/view'])) + ), + { dispatch: false } + ); +} diff --git a/frontend/src/app/front-page/front-page.component.html b/frontend/src/app/front-page/front-page.component.html index bc75a9c..eca668e 100644 --- a/frontend/src/app/front-page/front-page.component.html +++ b/frontend/src/app/front-page/front-page.component.html @@ -41,12 +41,12 @@ nbButton status="success" (click)="sendFile($event)" - [disabled]="!isFileFetched" + [disabled]="!(isFileFetched$ | async)" > Wyƛlij! -

- {{ fileName }} +

+ {{ fileName$ | async }}

diff --git a/frontend/src/app/front-page/front-page.component.ts b/frontend/src/app/front-page/front-page.component.ts index 322c884..7f2baf0 100644 --- a/frontend/src/app/front-page/front-page.component.ts +++ b/frontend/src/app/front-page/front-page.component.ts @@ -1,27 +1,26 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; -import { SendDataService } from '../_services/send-data.service'; -import { SharedDataService } from '../_services/shared-data.service'; -import { HttpErrorResponse } from '@angular/common/http'; -import { NbToastrService } from '@nebular/theme'; +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { State } from '../reducers/index'; +import { Observable } from 'rxjs'; +import * as Selectors from '../selectors/front-page.selectors'; +import { fetchFile, sendFile } from '../actions/front-page.actions'; @Component({ selector: 'app-front-page', templateUrl: './front-page.component.html', styleUrls: ['./front-page.component.scss'], }) -export class FrontPageComponent { - private file: File; - public fileName: string; - public isFileFetched: boolean; +export class FrontPageComponent implements OnInit { + file$: Observable; + fileName$: Observable; + isFileFetched$: Observable; - constructor( - private sendDataService: SendDataService, - private sharedDataService: SharedDataService, - private router: Router, - private toastService: NbToastrService - ) { - this.isFileFetched = false; + constructor(private store: Store) {} + + ngOnInit() { + this.file$ = this.store.select(Selectors.selectFile); + this.fileName$ = this.store.select(Selectors.selectFileName); + this.isFileFetched$ = this.store.select(Selectors.selectFetchStatus); } scrollTo(el: HTMLElement) { @@ -38,24 +37,10 @@ export class FrontPageComponent { } fetchFile(event: any): void { - this.file = event.target.files[0]; - this.fileName = this.file.name; - this.isFileFetched = true; + this.store.dispatch(fetchFile({ file: event.target.files[0] })); } sendFile(event: any): void { - this.sendDataService.postFile(this.file).subscribe( - (res: any) => { - this.sharedDataService.setData(res); - this.router.navigate(['/view']); - }, - (error: HttpErrorResponse) => { - if (error.status === 406) { - this.toastService.danger('', 'Format pliku jest niepoprawny!', { - icon: 'alert-circle', - }); - } - } - ); + this.store.dispatch({ type: '[FrontPage Component] Send File' }); } } diff --git a/frontend/src/app/front-page/front-page.module.ts b/frontend/src/app/front-page/front-page.module.ts index 32131dd..5747519 100644 --- a/frontend/src/app/front-page/front-page.module.ts +++ b/frontend/src/app/front-page/front-page.module.ts @@ -9,6 +9,8 @@ import { NbIconModule, NbStepperModule, } from '@nebular/theme'; +import { EffectsModule } from '@ngrx/effects'; +import { FrontPageEffect } from '../effects/front-page.effects'; @NgModule({ declarations: [FrontPageComponent], @@ -19,6 +21,7 @@ import { NbCardModule, NbIconModule, NbStepperModule, + EffectsModule.forFeature([FrontPageEffect]), ], providers: [SendDataService], }) diff --git a/frontend/src/app/reducers/front-page.reducers.ts b/frontend/src/app/reducers/front-page.reducers.ts new file mode 100644 index 0000000..956ac7c --- /dev/null +++ b/frontend/src/app/reducers/front-page.reducers.ts @@ -0,0 +1,46 @@ +import { Action, createReducer, on } from '@ngrx/store'; +import * as Actions from '../actions/front-page.actions'; + +export interface State { + file: File; + fileName: string; + isFileFetched: boolean; +} + +export interface DataState { + data: any; +} + +export const initialState: State = { + file: {} as File, + fileName: '', + isFileFetched: false, +}; + +export const initialDataState: DataState = { + data: '', +}; + +const _fileReducer = createReducer( + initialState, + on(Actions.fetchFile, (_, { file }) => ({ + file: file, + fileName: file.name, + isFileFetched: true, + })) +); + +const _dataReducer = createReducer( + initialDataState, + on(Actions.sendFileSuccess, (_, { data }) => { + return { data: data }; + }) +); + +export function fileReducer(state: State | undefined, action: Action) { + return _fileReducer(state, action); +} + +export function dataReducer(state: DataState | undefined, action: Action) { + return _dataReducer(state, action); +} diff --git a/frontend/src/app/reducers/index.ts b/frontend/src/app/reducers/index.ts new file mode 100644 index 0000000..3b4156e --- /dev/null +++ b/frontend/src/app/reducers/index.ts @@ -0,0 +1,31 @@ +import { ActionReducer, ActionReducerMap, MetaReducer } from '@ngrx/store'; +import { environment } from '../../environments/environment'; +import { + fileReducer, + State as FileState, + DataState, + dataReducer, +} from './front-page.reducers'; + +export function debug(reducer: ActionReducer): ActionReducer { + return function (state, action) { + console.log('State: ', state); + console.log('Action: ', action); + + return reducer(state, action); + }; +} + +export interface State { + fileState: FileState; + data: DataState; +} + +export const reducers: ActionReducerMap = { + fileState: fileReducer, + data: dataReducer, +}; + +export const metaReducers: MetaReducer[] = !environment.production + ? [debug] + : []; diff --git a/frontend/src/app/selectors/front-page.selectors.ts b/frontend/src/app/selectors/front-page.selectors.ts new file mode 100644 index 0000000..fddd7af --- /dev/null +++ b/frontend/src/app/selectors/front-page.selectors.ts @@ -0,0 +1,22 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { State as AppState } from '../reducers/index'; +import { State as FileState } from '../reducers/front-page.reducers'; + +export const selectFeature = createFeatureSelector( + 'fileState' +); + +export const selectFile = createSelector( + selectFeature, + (state: FileState) => state.file +); + +export const selectFileName = createSelector( + selectFeature, + (state: FileState) => state.fileName +); + +export const selectFetchStatus = createSelector( + selectFeature, + (state: FileState) => state.isFileFetched +);