diff --git a/module-frontend/package-lock.json b/module-frontend/package-lock.json
index 458568b..76df3a4 100644
--- a/module-frontend/package-lock.json
+++ b/module-frontend/package-lock.json
@@ -18,6 +18,8 @@
"@angular/platform-browser": "^16.1.0",
"@angular/platform-browser-dynamic": "^16.1.0",
"@angular/router": "^16.1.0",
+ "@ngrx/effects": "^16.1.0",
+ "@ngrx/store": "^16.1.0",
"keycloak-angular": "^14.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -27,6 +29,7 @@
"@angular-devkit/build-angular": "^16.1.4",
"@angular/cli": "~16.1.4",
"@angular/compiler-cli": "^16.1.0",
+ "@angular/localize": "^16.1.5",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
@@ -414,6 +417,29 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
+ "node_modules/@angular/localize": {
+ "version": "16.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz",
+ "integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "7.22.5",
+ "fast-glob": "3.2.12",
+ "yargs": "^17.2.1"
+ },
+ "bin": {
+ "localize-extract": "tools/bundles/src/extract/cli.js",
+ "localize-migrate": "tools/bundles/src/migrate/cli.js",
+ "localize-translate": "tools/bundles/src/translate/cli.js"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.10.0"
+ },
+ "peerDependencies": {
+ "@angular/compiler": "16.1.5",
+ "@angular/compiler-cli": "16.1.5"
+ }
+ },
"node_modules/@angular/material": {
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
@@ -3650,6 +3676,31 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/@ngrx/effects": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz",
+ "integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "^16.0.0",
+ "@ngrx/store": "16.1.0",
+ "rxjs": "^6.5.3 || ^7.5.0"
+ }
+ },
+ "node_modules/@ngrx/store": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz",
+ "integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "^16.0.0",
+ "rxjs": "^6.5.3 || ^7.5.0"
+ }
+ },
"node_modules/@ngtools/webpack": {
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
@@ -12593,6 +12644,17 @@
"tslib": "^2.3.0"
}
},
+ "@angular/localize": {
+ "version": "16.1.5",
+ "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-16.1.5.tgz",
+ "integrity": "sha512-8ApTdmv4sH0VbW9kVNanze5DEmb3OPIGzbD19jzvUSb6mTVMfUcQrsf4h+H8+cT+epBhor8RgVeVbUJaUbaLNQ==",
+ "dev": true,
+ "requires": {
+ "@babel/core": "7.22.5",
+ "fast-glob": "3.2.12",
+ "yargs": "^17.2.1"
+ }
+ },
"@angular/material": {
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.5.tgz",
@@ -14998,6 +15060,22 @@
"tslib": "^2.1.0"
}
},
+ "@ngrx/effects": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-16.1.0.tgz",
+ "integrity": "sha512-sdLzxjdQcYht3SYiuhchF4uOxxZIy3h9TEoyVp6PZJmiLJqhMAmV9/+s3tPD7yKd4+4o4n+HeiLDOTlMxn3Bxg==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
+ "@ngrx/store": {
+ "version": "16.1.0",
+ "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-16.1.0.tgz",
+ "integrity": "sha512-cGwI5wy+irIudQubfrbaKrDbJxCvu+lZYlBmsOlpzevQLCa+ZjNLrN05J025P3KvUmfzX4StEpAc8Ord089Kig==",
+ "requires": {
+ "tslib": "^2.0.0"
+ }
+ },
"@ngtools/webpack": {
"version": "16.1.4",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.4.tgz",
diff --git a/module-frontend/package.json b/module-frontend/package.json
index 4df3b3b..a4acdea 100644
--- a/module-frontend/package.json
+++ b/module-frontend/package.json
@@ -20,6 +20,8 @@
"@angular/platform-browser": "^16.1.0",
"@angular/platform-browser-dynamic": "^16.1.0",
"@angular/router": "^16.1.0",
+ "@ngrx/effects": "^16.1.0",
+ "@ngrx/store": "^16.1.0",
"keycloak-angular": "^14.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -29,6 +31,7 @@
"@angular-devkit/build-angular": "^16.1.4",
"@angular/cli": "~16.1.4",
"@angular/compiler-cli": "^16.1.0",
+ "@angular/localize": "^16.1.5",
"@types/jasmine": "~4.3.0",
"jasmine-core": "~4.6.0",
"karma": "~6.4.0",
diff --git a/module-frontend/src/app/app-routing.module.ts b/module-frontend/src/app/app-routing.module.ts
index 9f29392..175171f 100644
--- a/module-frontend/src/app/app-routing.module.ts
+++ b/module-frontend/src/app/app-routing.module.ts
@@ -1,23 +1,16 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {DashboardComponent} from "./dashboard/dashboard.component";
-import {ElectionsComponent} from "./elections/elections.component";
-import {AppAuthGuard} from "./auth/auth.guard";
import {PermissionDeniedComponent} from "./permission-denied/permission-denied.component";
const routes: Routes = [
{
path: '',
component: DashboardComponent,
- canActivate: [AppAuthGuard],
children: [
{
path: 'elections',
- component: ElectionsComponent,
- data: {
- roles: ['USER']
- },
- canActivate: [AppAuthGuard]
+ loadChildren: () => import("./elections/elections.module").then(mod => mod.ElectionsModule)
},
{
path: 'permission-denied',
diff --git a/module-frontend/src/app/app.component.html b/module-frontend/src/app/app.component.html
index a419b82..4683f1c 100644
--- a/module-frontend/src/app/app.component.html
+++ b/module-frontend/src/app/app.component.html
@@ -1,4 +1,3 @@
-
diff --git a/module-frontend/src/app/app.module.ts b/module-frontend/src/app/app.module.ts
index de56c32..c382010 100644
--- a/module-frontend/src/app/app.module.ts
+++ b/module-frontend/src/app/app.module.ts
@@ -11,45 +11,45 @@ import {MatGridListModule} from "@angular/material/grid-list";
import {MatIconModule} from "@angular/material/icon";
import {MatListModule} from "@angular/material/list";
import {KeycloakAngularModule, KeycloakService} from "keycloak-angular";
-import {ElectionsComponent} from './elections/elections.component';
import {PermissionDeniedComponent} from './permission-denied/permission-denied.component';
-import {MessagesComponent} from './messages/messages.component';
import {HttpClientModule} from "@angular/common/http";
import {environment} from "../environments/environment";
import {NavigationComponent} from './navigation/navigation.component';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatMenuModule} from "@angular/material/menu";
import {MatTableModule} from "@angular/material/table";
+import {StoreModule} from '@ngrx/store';
+import {ElectionsModule} from "./elections/elections.module";
+import {EffectsModule} from '@ngrx/effects';
+import {MessagesModule} from "./messages/messages.module";
function initializeKeycloak(keycloak: KeycloakService) {
return () =>
- keycloak.init({
- config: {
- url: environment.keycloakURL,
- realm: environment.realm,
- clientId: environment.clientId,
- },
- initOptions: {
- onLoad: 'check-sso',
- silentCheckSsoRedirectUri:
- window.location.origin + '/assets/silent-check-sso.html',
- flow: "standard"
- },
- shouldAddToken: (request) => {
- const { url } = request;
- return url.startsWith(environment.backendURL);
- },
- loadUserProfileAtStartUp: true
- });
+ keycloak.init({
+ config: {
+ url: environment.keycloakURL,
+ realm: environment.realm,
+ clientId: environment.clientId,
+ },
+ initOptions: {
+ onLoad: 'check-sso',
+ silentCheckSsoRedirectUri:
+ window.location.origin + '/assets/silent-check-sso.html',
+ flow: "standard"
+ },
+ shouldAddToken: (request) => {
+ const {url} = request;
+ return url.startsWith(environment.backendURL);
+ },
+ loadUserProfileAtStartUp: true
+ });
}
@NgModule({
declarations: [
AppComponent,
DashboardComponent,
- ElectionsComponent,
PermissionDeniedComponent,
- MessagesComponent,
NavigationComponent
],
imports: [
@@ -66,7 +66,11 @@ function initializeKeycloak(keycloak: KeycloakService) {
HttpClientModule,
MatSidenavModule,
MatMenuModule,
- MatTableModule
+ MatTableModule,
+ ElectionsModule,
+ MessagesModule,
+ StoreModule.forRoot({}, {}),
+ EffectsModule.forRoot([])
],
providers: [
{
diff --git a/module-frontend/src/app/dashboard/dashboard.component.ts b/module-frontend/src/app/dashboard/dashboard.component.ts
index f7bd656..58bb724 100644
--- a/module-frontend/src/app/dashboard/dashboard.component.ts
+++ b/module-frontend/src/app/dashboard/dashboard.component.ts
@@ -1,7 +1,7 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {ElectionService} from "../elections/election.service";
-import {Election} from "../elections/election";
+import {Election} from "../elections/model/election";
import {Subscription} from "rxjs";
@Component({
diff --git a/module-frontend/src/app/elections/election.service.ts b/module-frontend/src/app/elections/election.service.ts
index 2623a1c..4d27b4f 100644
--- a/module-frontend/src/app/elections/election.service.ts
+++ b/module-frontend/src/app/elections/election.service.ts
@@ -1,9 +1,12 @@
import {Injectable} from '@angular/core';
import {HttpClient, HttpHeaders} from "@angular/common/http";
import {environment} from "../../environments/environment";
-import {Election} from "./election";
-import {catchError, Observable, of, tap} from "rxjs";
-import {MessageService} from "../messages/message.service";
+import {Election} from "./model/election";
+import {catchError, Observable, of} from "rxjs";
+import {Store} from "@ngrx/store";
+import {electionByName} from "./store";
+import {MessagesService} from "../messages/messages.service";
+import {MessageType} from "../messages/model/message-type";
@Injectable({
providedIn: 'root'
@@ -17,16 +20,20 @@ export class ElectionService {
};
constructor(private http: HttpClient,
- private messageService: MessageService) { }
+ private store: Store,
+ private messageService: MessagesService) { }
getElections(): Observable {
- return this.http.get(this.electionsURL)
+ return this.http.get(this.electionsURL, this.httpOptions)
.pipe(
- tap(_ => this.log('fetched elections')),
catchError(this.handleError('getElections', []))
);
}
+ selectElection(name: string) {
+ return this.store.select(electionByName(name));
+ }
+
/**
* Handle Http operation that failed.
* Let the app continue.
@@ -39,16 +46,22 @@ export class ElectionService {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
- // TODO: better job of transforming error for user consumption
- this.log(`${operation} failed: ${error.message}`);
+ if (error.status == 401) {
+ this.log(MessageType.UNAUTHENTICATED);
+ }
+ if (error.status == 403) {
+ this.log(MessageType.UNAUTHORIZED);
+ }
+ if (error.status == 500) {
+ this.log(MessageType.INTERNAL_SERVER_ERROR);
+ }
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
- /** Log a HeroService message with the MessageService */
- private log(message: string) {
- this.messageService.add(`ElectionService: ${message}`);
+ private log(type: MessageType) {
+ this.messageService.logMessage('ElectionService', type);
}
}
diff --git a/module-frontend/src/app/elections/elections-routing.module.ts b/module-frontend/src/app/elections/elections-routing.module.ts
new file mode 100644
index 0000000..c7444ac
--- /dev/null
+++ b/module-frontend/src/app/elections/elections-routing.module.ts
@@ -0,0 +1,17 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from "@angular/router";
+import {ElectionsComponent} from "./elections.component";
+
+const routes: Routes = [
+ {
+ path: '',
+ component: ElectionsComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ElectionsRoutingModule {
+}
diff --git a/module-frontend/src/app/elections/elections.component.ts b/module-frontend/src/app/elections/elections.component.ts
index bed3497..c4a4fcf 100644
--- a/module-frontend/src/app/elections/elections.component.ts
+++ b/module-frontend/src/app/elections/elections.component.ts
@@ -1,8 +1,10 @@
import {Component, OnInit} from '@angular/core';
import {KeycloakService} from "keycloak-angular";
-import {Election} from "./election";
-import {ElectionService} from "./election.service";
+import {Election} from "./model/election";
import {Observable} from "rxjs";
+import {Store} from "@ngrx/store";
+import {allElections} from "./store";
+import {loadAllElectionsAction} from "./store/elections.actions";
@Component({
selector: 'app-elections',
@@ -10,12 +12,13 @@ import {Observable} from "rxjs";
styleUrls: ['./elections.component.scss']
})
export class ElectionsComponent implements OnInit{
- $elections: Observable = this.electionService.getElections();
+ $elections: Observable = this.store.select(allElections());
constructor(private keycloakService: KeycloakService,
- private electionService: ElectionService) {
+ private store: Store) {
}
ngOnInit() {
+ this.store.dispatch(loadAllElectionsAction());
}
}
diff --git a/module-frontend/src/app/elections/elections.module.ts b/module-frontend/src/app/elections/elections.module.ts
new file mode 100644
index 0000000..ec26fc8
--- /dev/null
+++ b/module-frontend/src/app/elections/elections.module.ts
@@ -0,0 +1,26 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {ElectionsComponent} from "./elections.component";
+import {StoreModule} from "@ngrx/store";
+import {ElectionsEffects} from "./store/elections.effects";
+import {EffectsModule} from "@ngrx/effects";
+import {electionsReducers, featureStateName} from "./store";
+import {ElectionsRoutingModule} from "./elections-routing.module";
+
+
+@NgModule({
+ declarations: [
+ ElectionsComponent
+ ],
+ imports: [
+ CommonModule,
+ ElectionsRoutingModule,
+ StoreModule.forFeature(featureStateName, electionsReducers),
+ EffectsModule.forFeature([ElectionsEffects]),
+ ],
+ exports: [
+ ElectionsComponent
+ ]
+})
+export class ElectionsModule {
+}
diff --git a/module-frontend/src/app/elections/constituency.ts b/module-frontend/src/app/elections/model/constituency.ts
similarity index 100%
rename from module-frontend/src/app/elections/constituency.ts
rename to module-frontend/src/app/elections/model/constituency.ts
diff --git a/module-frontend/src/app/elections/election.ts b/module-frontend/src/app/elections/model/election.ts
similarity index 100%
rename from module-frontend/src/app/elections/election.ts
rename to module-frontend/src/app/elections/model/election.ts
diff --git a/module-frontend/src/app/elections/voting-threshold.ts b/module-frontend/src/app/elections/model/voting-threshold.ts
similarity index 100%
rename from module-frontend/src/app/elections/voting-threshold.ts
rename to module-frontend/src/app/elections/model/voting-threshold.ts
diff --git a/module-frontend/src/app/elections/store/elections.actions.ts b/module-frontend/src/app/elections/store/elections.actions.ts
new file mode 100644
index 0000000..f6c42de
--- /dev/null
+++ b/module-frontend/src/app/elections/store/elections.actions.ts
@@ -0,0 +1,43 @@
+import {createAction, props} from "@ngrx/store";
+import {Election} from "../model/election";
+
+export enum ActionTypes {
+ LoadAllElections = '[Elections] Load All Elections',
+ LoadAllElectionsFinished = '[Elections] Load All Elections Finished',
+
+ LoadSingleElection = '[Elections] Load Single Election',
+ LoadSingleElectionFinished = '[Elections] Load Single Election Finished',
+}
+
+export const loadAllElectionsAction = createAction(
+ ActionTypes.LoadAllElections
+)
+
+export type LoadAllElectionsAction = ReturnType;
+
+export const loadAllElectionsFinishedAction = createAction(
+ ActionTypes.LoadAllElectionsFinished,
+ props<{payload: Election[]}>()
+)
+
+export type LoadAllElectionsFinishedAction = ReturnType;
+
+export const loadSingleElectionAction = createAction(
+ ActionTypes.LoadSingleElection,
+ props<{payload: string}>()
+)
+
+export type LoadSingleElectionAction = ReturnType;
+
+export const loadSingleElectionFinishedAction = createAction(
+ ActionTypes.LoadSingleElectionFinished,
+ props<{payload: Election|undefined}>()
+)
+
+export type LoadSingleElectionFinishedAction = ReturnType;
+
+export type ElectionsActions =
+ | LoadAllElectionsAction
+ | LoadAllElectionsFinishedAction
+ | LoadSingleElectionAction
+ | LoadSingleElectionFinishedAction;
diff --git a/module-frontend/src/app/elections/store/elections.effects.ts b/module-frontend/src/app/elections/store/elections.effects.ts
new file mode 100644
index 0000000..f93303f
--- /dev/null
+++ b/module-frontend/src/app/elections/store/elections.effects.ts
@@ -0,0 +1,38 @@
+import {Injectable} from "@angular/core";
+import {ElectionService} from "../election.service";
+import {
+ ActionTypes,
+ loadAllElectionsFinishedAction,
+ LoadSingleElectionAction,
+ loadSingleElectionFinishedAction
+} from "./elections.actions";
+import {map, switchMap} from "rxjs";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+
+@Injectable()
+export class ElectionsEffects {
+ constructor(private actions$: Actions,
+ private electionService: ElectionService) {
+ }
+
+ loadAllElections$ = createEffect(() => this.actions$.pipe(
+ ofType(ActionTypes.LoadAllElections),
+ switchMap(() => this.electionService.getElections()
+ .pipe(
+ map(elections => loadAllElectionsFinishedAction({payload: elections}))
+ )
+ )
+ ));
+
+ loadSingleElection$ = createEffect(() => {
+ return this.actions$.pipe(
+ ofType(ActionTypes.LoadSingleElection),
+ switchMap((action: LoadSingleElectionAction) => this.electionService.selectElection(action.payload)
+ .pipe(
+ map(election =>
+ loadSingleElectionFinishedAction({payload: election}))
+ )
+ ),
+ )
+ });
+}
diff --git a/module-frontend/src/app/elections/store/elections.reducer.ts b/module-frontend/src/app/elections/store/elections.reducer.ts
new file mode 100644
index 0000000..e5aa9db
--- /dev/null
+++ b/module-frontend/src/app/elections/store/elections.reducer.ts
@@ -0,0 +1,30 @@
+import {Election} from "../model/election";
+import {
+ loadAllElectionsFinishedAction,
+ loadSingleElectionFinishedAction
+} from "./elections.actions";
+import {createReducer, on} from "@ngrx/store";
+
+export interface ReducerElectionsState {
+ items: Election[];
+ selectedItem: Election|undefined;
+}
+
+export const initialState: ReducerElectionsState = {
+ items: [],
+ selectedItem: undefined
+};
+
+export const electionsReducer = createReducer(
+ initialState,
+ on(loadAllElectionsFinishedAction, (state,
+ action) => ({
+ ...state,
+ items: [...action.payload]
+ })),
+ on(loadSingleElectionFinishedAction, (state,
+ action) => ({
+ ...state,
+ selectedItem: action.payload
+ }))
+);
diff --git a/module-frontend/src/app/elections/store/index.ts b/module-frontend/src/app/elections/store/index.ts
new file mode 100644
index 0000000..7a7facc
--- /dev/null
+++ b/module-frontend/src/app/elections/store/index.ts
@@ -0,0 +1,28 @@
+import {electionsReducer, ReducerElectionsState} from "./elections.reducer";
+import {ActionReducerMap, createFeatureSelector, createSelector} from "@ngrx/store";
+
+export const featureStateName = 'electionsFeature';
+
+export interface ElectionsState {
+ elections: ReducerElectionsState;
+}
+
+export const electionsReducers: ActionReducerMap = {
+ elections: electionsReducer,
+};
+
+// extract the main property 'electionsFeature' from the state object
+export const getElectionsFeatureState = createFeatureSelector(
+ featureStateName
+);
+
+export const allElections = () => createSelector(
+ getElectionsFeatureState,
+ (state: ElectionsState) => state.elections.items
+);
+
+export const electionByName = (name: string) => createSelector(
+ getElectionsFeatureState,
+ (state: ElectionsState) =>
+ state.elections.items.find(election => election.name === name)
+);
diff --git a/module-frontend/src/app/messages/message.service.ts b/module-frontend/src/app/messages/message.service.ts
deleted file mode 100644
index d4d05b4..0000000
--- a/module-frontend/src/app/messages/message.service.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {Injectable} from '@angular/core';
-
-@Injectable({
- providedIn: 'root'
-})
-export class MessageService {
- messages: string[] = [];
-
- add(message: string) {
- this.messages.push(message);
- }
-
- clear() {
- this.messages = [];
- }
-}
diff --git a/module-frontend/src/app/messages/messages.component.html b/module-frontend/src/app/messages/messages.component.html
deleted file mode 100644
index 1df7dfd..0000000
--- a/module-frontend/src/app/messages/messages.component.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
Messages
-
-
{{message}}
-
-
diff --git a/module-frontend/src/app/messages/messages.component.scss b/module-frontend/src/app/messages/messages.component.scss
deleted file mode 100644
index d1c36dc..0000000
--- a/module-frontend/src/app/messages/messages.component.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-h2 {
- color: red;
- font-family: Arial, Helvetica, sans-serif;
- font-weight: lighter;
-}
-body {
- margin: 2em;
-}
-body, input[text], button {
- color: crimson;
- font-family: Cambria, Georgia, sans-serif;
-}
-
-button.clear {
- font-family: Arial, sans-serif;
- background-color: #eee;
- border: none;
- padding: 5px 10px;
- border-radius: 4px;
- cursor: pointer;
-}
-button:hover {
- background-color: #cfd8dc;
-}
-button:disabled {
- background-color: #eee;
- color: #aaa;
- cursor: auto;
-}
-button.clear {
- color: #333;
- margin-bottom: 12px;
-}
diff --git a/module-frontend/src/app/messages/messages.component.spec.ts b/module-frontend/src/app/messages/messages.component.spec.ts
deleted file mode 100644
index 9df302d..0000000
--- a/module-frontend/src/app/messages/messages.component.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import {ComponentFixture, TestBed} from '@angular/core/testing';
-
-import {MessagesComponent} from './messages.component';
-
-describe('MessagesComponent', () => {
- let component: MessagesComponent;
- let fixture: ComponentFixture;
-
- beforeEach(() => {
- TestBed.configureTestingModule({
- declarations: [MessagesComponent]
- });
- fixture = TestBed.createComponent(MessagesComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/module-frontend/src/app/messages/messages.component.ts b/module-frontend/src/app/messages/messages.component.ts
deleted file mode 100644
index f844f1f..0000000
--- a/module-frontend/src/app/messages/messages.component.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import {Component} from '@angular/core';
-import {MessageService} from "./message.service";
-
-@Component({
- selector: 'app-messages',
- templateUrl: './messages.component.html',
- styleUrls: ['./messages.component.scss']
-})
-export class MessagesComponent {
- constructor(public messageService: MessageService) {}
-}
diff --git a/module-frontend/src/app/messages/messages.module.ts b/module-frontend/src/app/messages/messages.module.ts
new file mode 100644
index 0000000..13d9472
--- /dev/null
+++ b/module-frontend/src/app/messages/messages.module.ts
@@ -0,0 +1,20 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {StoreModule} from "@ngrx/store";
+import {featureStateName, messagesReducers} from "./store";
+import {EffectsModule} from "@ngrx/effects";
+import {MessagesEffects} from "./store/messages.effects";
+import {MatSnackBarModule} from "@angular/material/snack-bar";
+
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule,
+ MatSnackBarModule,
+ StoreModule.forFeature(featureStateName, messagesReducers),
+ EffectsModule.forFeature([MessagesEffects])
+ ]
+})
+export class MessagesModule {
+}
diff --git a/module-frontend/src/app/messages/message.service.spec.ts b/module-frontend/src/app/messages/messages.service.spec.ts
similarity index 53%
rename from module-frontend/src/app/messages/message.service.spec.ts
rename to module-frontend/src/app/messages/messages.service.spec.ts
index ce1f5d1..bf46410 100644
--- a/module-frontend/src/app/messages/message.service.spec.ts
+++ b/module-frontend/src/app/messages/messages.service.spec.ts
@@ -1,13 +1,13 @@
import {TestBed} from '@angular/core/testing';
-import {MessageService} from './message.service';
+import {MessagesService} from './messages.service';
-describe('MessageService', () => {
- let service: MessageService;
+describe('MessagesService', () => {
+ let service: MessagesService;
beforeEach(() => {
TestBed.configureTestingModule({});
- service = TestBed.inject(MessageService);
+ service = TestBed.inject(MessagesService);
});
it('should be created', () => {
diff --git a/module-frontend/src/app/messages/messages.service.ts b/module-frontend/src/app/messages/messages.service.ts
new file mode 100644
index 0000000..8ad636f
--- /dev/null
+++ b/module-frontend/src/app/messages/messages.service.ts
@@ -0,0 +1,34 @@
+import {Injectable} from '@angular/core';
+import {Store} from "@ngrx/store";
+import {addMessageAction} from "./store/messages.actions";
+import {MessageType} from "./model/message-type";
+
+@Injectable({
+ providedIn: 'root'
+})
+export class MessagesService {
+ private static UNAUTHENTICATED = $localize `You are not logged in which prevents data from loading`;
+ private static UNAUTHORIZED = $localize `You don't have sufficient authorization to view the data`;
+ private static INTERNAL_SERVER_ERROR = $localize `An internal server error occurred. Sorry for the inconvenience.`;
+ private static UNKNOWN_ERROR = $localize `An unknown error occurred. Sorry for the inconvenience.`;
+
+ constructor(private store: Store) { }
+
+ logMessage(component: string, type: MessageType) {
+ let text = component + ": ";
+ switch (type) {
+ case MessageType.UNAUTHENTICATED:
+ text += MessagesService.UNAUTHENTICATED;
+ break;
+ case MessageType.UNAUTHORIZED:
+ text += MessagesService.UNAUTHORIZED;
+ break;
+ case MessageType.INTERNAL_SERVER_ERROR:
+ text += MessagesService.INTERNAL_SERVER_ERROR;
+ break;
+ default:
+ text += MessagesService.UNKNOWN_ERROR;
+ }
+ this.store.dispatch(addMessageAction({message: {text, type}}))
+ }
+}
diff --git a/module-frontend/src/app/messages/model/message-type.ts b/module-frontend/src/app/messages/model/message-type.ts
new file mode 100644
index 0000000..5dd44d6
--- /dev/null
+++ b/module-frontend/src/app/messages/model/message-type.ts
@@ -0,0 +1,5 @@
+export enum MessageType {
+ UNAUTHENTICATED,
+ UNAUTHORIZED,
+ INTERNAL_SERVER_ERROR
+}
diff --git a/module-frontend/src/app/messages/model/message.ts b/module-frontend/src/app/messages/model/message.ts
new file mode 100644
index 0000000..443155c
--- /dev/null
+++ b/module-frontend/src/app/messages/model/message.ts
@@ -0,0 +1,6 @@
+import {MessageType} from "./message-type";
+
+export interface Message {
+ text: string;
+ type: MessageType;
+}
diff --git a/module-frontend/src/app/messages/store/index.ts b/module-frontend/src/app/messages/store/index.ts
new file mode 100644
index 0000000..528dec6
--- /dev/null
+++ b/module-frontend/src/app/messages/store/index.ts
@@ -0,0 +1,16 @@
+import {messagesReducer, ReducerMessagesState} from "./messages.reducer";
+import {ActionReducerMap, createFeatureSelector} from "@ngrx/store";
+
+export const featureStateName = 'messagesFeature';
+
+export interface MessagesState {
+ messages: ReducerMessagesState;
+}
+
+export const messagesReducers: ActionReducerMap = {
+ messages: messagesReducer,
+};
+
+export const getMessagesFeatureState = createFeatureSelector(
+ featureStateName
+);
diff --git a/module-frontend/src/app/messages/store/messages.actions.ts b/module-frontend/src/app/messages/store/messages.actions.ts
new file mode 100644
index 0000000..807d824
--- /dev/null
+++ b/module-frontend/src/app/messages/store/messages.actions.ts
@@ -0,0 +1,24 @@
+import {createAction, props} from "@ngrx/store";
+import {Message} from "../model/message";
+
+export enum ActionTypes {
+ AddMessage = '[Messages] Add Message',
+ AddMessageFinished = '[Messages] Add Message Finished',
+}
+
+export const addMessageAction = createAction(
+ ActionTypes.AddMessage,
+ props<{message: Message}>()
+);
+
+export type AddMessageAction = ReturnType;
+
+export const addMessageFinishedAction = createAction(
+ ActionTypes.AddMessageFinished
+);
+
+export type AddMessageFinishedAction = ReturnType;
+
+export type MessagesActions =
+ | AddMessageAction
+ | AddMessageFinishedAction;
diff --git a/module-frontend/src/app/messages/store/messages.effects.ts b/module-frontend/src/app/messages/store/messages.effects.ts
new file mode 100644
index 0000000..ac71b79
--- /dev/null
+++ b/module-frontend/src/app/messages/store/messages.effects.ts
@@ -0,0 +1,21 @@
+import {MatSnackBar} from "@angular/material/snack-bar";
+import {Actions, createEffect, ofType} from "@ngrx/effects";
+import {ActionTypes, AddMessageAction, addMessageFinishedAction} from "./messages.actions";
+import {map} from "rxjs";
+import {Injectable} from "@angular/core";
+
+@Injectable()
+export class MessagesEffects {
+ constructor(private actions$: Actions,
+ private snackBar: MatSnackBar) {
+ }
+
+ showSnackbar$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(ActionTypes.AddMessage),
+ map((action: AddMessageAction) => {
+ this.snackBar.open(action.message.text, "OK");
+ return addMessageFinishedAction();
+ })
+ ));
+}
diff --git a/module-frontend/src/app/messages/store/messages.reducer.ts b/module-frontend/src/app/messages/store/messages.reducer.ts
new file mode 100644
index 0000000..9ef2a1e
--- /dev/null
+++ b/module-frontend/src/app/messages/store/messages.reducer.ts
@@ -0,0 +1,24 @@
+import {Message} from "../model/message";
+import {createReducer, on} from "@ngrx/store";
+import {addMessageAction, addMessageFinishedAction} from "./messages.actions";
+
+export interface ReducerMessagesState {
+ displayedMessage: Message|undefined;
+}
+
+const initialState: ReducerMessagesState = {
+ displayedMessage: undefined
+}
+
+export const messagesReducer = createReducer(
+ initialState,
+ on(addMessageAction, (state,
+ action) => ({
+ ...state,
+ displayedMessage: action.message
+ })),
+ on(addMessageFinishedAction, (state) => ({
+ ...state,
+ displayedMessage: undefined
+ }))
+);
diff --git a/module-frontend/src/app/navigation/navigation.component.html b/module-frontend/src/app/navigation/navigation.component.html
index d6fc591..5716b54 100644
--- a/module-frontend/src/app/navigation/navigation.component.html
+++ b/module-frontend/src/app/navigation/navigation.component.html
@@ -20,8 +20,13 @@
Wahlrecht
- Logged in as {{loggedUserName}}
-
+
+ Logged in as {{loggedUserName}}
+
+
+
+
+
diff --git a/module-frontend/src/app/navigation/navigation.component.ts b/module-frontend/src/app/navigation/navigation.component.ts
index 63c82a5..4e4f5b1 100644
--- a/module-frontend/src/app/navigation/navigation.component.ts
+++ b/module-frontend/src/app/navigation/navigation.component.ts
@@ -1,5 +1,7 @@
import {Component} from '@angular/core';
import {KeycloakService} from "keycloak-angular";
+import {from} from "rxjs";
+import {ActivatedRoute} from "@angular/router";
@Component({
selector: 'app-navigation',
@@ -8,13 +10,24 @@ import {KeycloakService} from "keycloak-angular";
})
export class NavigationComponent {
loggedUserName = '';
+ isLoggedIn$ = from(this.keycloakService.isLoggedIn());
+ url: string;
- constructor(private keycloakService: KeycloakService) { }
+ constructor(private keycloakService: KeycloakService,
+ private route: ActivatedRoute) {
+ this.url = route.snapshot.url.join('');
+ }
ngOnInit(): void {
this.loggedUserName = this.keycloakService.getUsername();
}
+ login(): void {
+ this.keycloakService.login({
+ redirectUri: window.location.origin + this.url,
+ });
+ }
+
logout(): void {
this.keycloakService.logout(window.location.origin);
}
diff --git a/module-frontend/src/app/store/app.reducer.ts b/module-frontend/src/app/store/app.reducer.ts
new file mode 100644
index 0000000..78e301c
--- /dev/null
+++ b/module-frontend/src/app/store/app.reducer.ts
@@ -0,0 +1,7 @@
+import {ElectionsState} from "../elections/store";
+import {MessagesState} from "../messages/store";
+
+export interface AppState {
+ electionsFeature: ElectionsState;
+ messagesFeature: MessagesState;
+}
diff --git a/module-frontend/src/app/store/index.ts b/module-frontend/src/app/store/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/module-frontend/src/main.ts b/module-frontend/src/main.ts
index e59cec9..c20ec05 100644
--- a/module-frontend/src/main.ts
+++ b/module-frontend/src/main.ts
@@ -1,3 +1,5 @@
+///
+
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
diff --git a/module-frontend/tsconfig.app.json b/module-frontend/tsconfig.app.json
index 374cc9d..ec26f70 100644
--- a/module-frontend/tsconfig.app.json
+++ b/module-frontend/tsconfig.app.json
@@ -3,7 +3,9 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
- "types": []
+ "types": [
+ "@angular/localize"
+ ]
},
"files": [
"src/main.ts"
diff --git a/module-frontend/tsconfig.spec.json b/module-frontend/tsconfig.spec.json
index be7e9da..c63b698 100644
--- a/module-frontend/tsconfig.spec.json
+++ b/module-frontend/tsconfig.spec.json
@@ -4,7 +4,8 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
- "jasmine"
+ "jasmine",
+ "@angular/localize"
]
},
"include": [