diff --git a/karma.conf.js b/karma.conf.js
index ecf8659..f390a6d 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -8,12 +8,16 @@ module.exports = function (config) {
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
+ require('karma-firefox-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
- clearContext: false // leave Jasmine Spec Runner output visible in browser
+ clearContext: false, // leave Jasmine Spec Runner output visible in browser
+ jasmine: {
+ random: false
+ }
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/fe-test-app'),
@@ -25,7 +29,7 @@ module.exports = function (config) {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
- browsers: ['Chrome'],
+ browsers: ['Firefox'],
singleRun: false,
restartOnFileChange: true
});
diff --git a/package-lock.json b/package-lock.json
index 4b7e90c..8c7f895 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
"requires": true,
"packages": {
"": {
+ "name": "fe-test-app",
"version": "1.0.0",
"dependencies": {
"@angular/animations": "~8.2.14",
@@ -43,6 +44,7 @@
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-firefox-launcher": "^2.1.2",
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.6.0",
"path-browserify": "^1.0.1",
@@ -6640,6 +6642,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -7441,6 +7458,43 @@
"minimatch": "^3.0.4"
}
},
+ "node_modules/karma-firefox-launcher": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz",
+ "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==",
+ "dev": true,
+ "dependencies": {
+ "is-wsl": "^2.2.0",
+ "which": "^2.0.1"
+ }
+ },
+ "node_modules/karma-firefox-launcher/node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/karma-firefox-launcher/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/karma-jasmine": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz",
@@ -20537,6 +20591,12 @@
"integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=",
"dev": true
},
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true
+ },
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -21464,6 +21524,36 @@
"minimatch": "^3.0.4"
}
},
+ "karma-firefox-launcher": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.2.tgz",
+ "integrity": "sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==",
+ "dev": true,
+ "requires": {
+ "is-wsl": "^2.2.0",
+ "which": "^2.0.1"
+ },
+ "dependencies": {
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "requires": {
+ "is-docker": "^2.0.0"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ }
+ }
+ },
"karma-jasmine": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-3.3.1.tgz",
diff --git a/package.json b/package.json
index 1d24dcd..292e0ff 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
+ "karma-firefox-launcher": "^2.1.2",
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.6.0",
"path-browserify": "^1.0.1",
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 06c7342..d606e9a 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -1,8 +1,22 @@
import { NgModule } from '@angular/core';
-import { Routes, RouterModule } from '@angular/router';
+import { RouterModule, Routes } from '@angular/router';
+import { CreateUserComponent } from './components/create-user/create-user.component';
+import { UsersTableComponent } from './components/users-table/users-table.component';
+import { APP_ROUTES } from './enums/app-routes.enum';
-const routes: Routes = [];
+const routes: Routes = [
+ {
+ path: APP_ROUTES.USERS,
+ component: UsersTableComponent,
+ },
+ {
+ path: APP_ROUTES.CREATE_USER,
+ component: CreateUserComponent,
+ pathMatch: 'full'
+ },
+ { path: '**', redirectTo: APP_ROUTES.USERS, pathMatch: 'full' }
+];
@NgModule({
imports: [RouterModule.forRoot(routes)],
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 90c6b64..b2d0dfa 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1 +1,3 @@
-
\ No newline at end of file
+{{'APP.TITLE'}}
+
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index e69de29..e679d90 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -0,0 +1,3 @@
+mat-progress-bar {
+ position: absolute;
+}
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index caa6302..1c2d85a 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -1,16 +1,36 @@
-import { TestBed, async } from '@angular/core/testing';
+import { async, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { MatProgressBarModule, MatToolbarModule } from '@angular/material';
import { RouterTestingModule } from '@angular/router/testing';
+import { of, BehaviorSubject } from 'rxjs';
import { AppComponent } from './app.component';
+import { LoadingService } from './services/loading.service';
describe('AppComponent', () => {
+
+ let _loading = new BehaviorSubject(false);
beforeEach(async(() => {
+ const mockLoadingService = jasmine.createSpyObj('LoadingService', ['isLoading', 'setLoading', 'loading']);
+
+ mockLoadingService.isLoading.and.callFake(function(){
+ return _loading;
+ });
+ mockLoadingService.setLoading.and.callFake(function(loading:boolean){
+ _loading.next(loading);
+ });
+ mockLoadingService.loading.and.callFake(function(){
+ return _loading.asObservable();
+ });
+
TestBed.configureTestingModule({
imports: [
- RouterTestingModule
+ RouterTestingModule,
+ MatToolbarModule,
+ MatProgressBarModule
],
declarations: [
AppComponent
],
+ providers: [{ provide: LoadingService, useValue: mockLoadingService }]
}).compileComponents();
}));
@@ -19,4 +39,36 @@ describe('AppComponent', () => {
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
+
+ it('should subscribe to loading events', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ app.ngOnInit();
+
+ expect(app.subscriptions).toBeTruthy();
+ expect(app.subscriptions.length).toBeGreaterThan(0);
+ expect(app.subscriptions[0].closed).toBeFalsy();
+ });
+
+ it('it should update loading value', (fakeAsync(() => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ const service = fixture.debugElement.injector.get(LoadingService);
+ app.ngOnInit();
+
+ expect(app.isLoading).toBe(false);
+ service.setLoading(true);
+ tick(150);
+ expect(app.isLoading).toBe(true);
+ })));
+
+ it('it should unsubscribe on destroy', () => {
+ const fixture = TestBed.createComponent(AppComponent);
+ const app = fixture.debugElement.componentInstance;
+ app.ngOnInit();
+
+ expect(app.subscriptions).toBeTruthy();
+ app.ngOnDestroy();
+ expect(app.subscriptions[0].closed).toBeTruthy();
+ });
});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 3b59aaa..29cc348 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,8 +1,32 @@
-import { Component } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { LoadingService } from './services/loading.service';
@Component({
selector: 'exads-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
-export class AppComponent { }
+export class AppComponent implements OnInit {
+
+ private subscriptions: Subscription[] = [];
+ public isLoading: boolean = false;
+ constructor(public loadingService: LoadingService) {
+ }
+ ngOnInit(): void {
+ this.subscriptions.push(
+ this.loadingService.loading().subscribe((loading) => {
+ // use short timeout to prevent ExpressionChangedAfterItHasBeenCheckedError
+ setTimeout(() => {
+ this.isLoading = loading;
+ }, 100)
+ })
+ );
+ }
+
+ ngOnDestroy(): void {
+ this.subscriptions.forEach(sub => {
+ sub.unsubscribe();
+ });
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 0cadd23..223d00f 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,20 +1,44 @@
-import { BrowserModule } from '@angular/platform-browser';
+import { LoadingInterceptor } from './interceptors/loading.interceptor';
+import { HttpClientModule, HTTP_INTERCEPTORS, HttpInterceptor } from '@angular/common/http';
import { NgModule } from '@angular/core';
-
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule, MatInputModule, MatPaginatorIntl, MatPaginatorModule, MatProgressBarModule, MatSelectModule, MatSnackBarModule, MatTableModule, MatToolbarModule } from '@angular/material';
+import { MatButtonModule } from '@angular/material/button';
+import { BrowserModule } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
-import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { CreateUserComponent } from './components/create-user/create-user.component';
+import { UsersTableComponent } from './components/users-table/users-table.component';
+import { CustomMatPagerIntl } from './mat-helpers/custom-mat-pager-intl';
+
@NgModule({
declarations: [
- AppComponent
+ AppComponent,
+ UsersTableComponent,
+ CreateUserComponent
],
imports: [
BrowserModule,
AppRoutingModule,
- BrowserAnimationsModule
+ BrowserAnimationsModule,
+ HttpClientModule,
+ MatTableModule,
+ MatPaginatorModule,
+ MatButtonModule,
+ MatFormFieldModule,
+ MatInputModule,
+ MatSelectModule,
+ ReactiveFormsModule,
+ MatToolbarModule,
+ MatProgressBarModule,
+ MatSnackBarModule
+ ],
+ providers: [
+ { provide: HTTP_INTERCEPTORS, useClass: LoadingInterceptor, multi: true },
+ { provide: MatPaginatorIntl, useClass: CustomMatPagerIntl}
],
- providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
diff --git a/src/app/components/create-user/create-user.component.html b/src/app/components/create-user/create-user.component.html
new file mode 100644
index 0000000..f2be7a3
--- /dev/null
+++ b/src/app/components/create-user/create-user.component.html
@@ -0,0 +1,36 @@
+
+
CREATE_USER.TITLE
+
+
+
+
+
+
+
diff --git a/src/app/components/create-user/create-user.component.scss b/src/app/components/create-user/create-user.component.scss
new file mode 100644
index 0000000..afa2d77
--- /dev/null
+++ b/src/app/components/create-user/create-user.component.scss
@@ -0,0 +1,41 @@
+h2 {
+ margin-top: 50px;
+ margin-left: 30px;
+ text-transform: capitalize;
+}
+.main-container {
+ background-color: rgb(231, 231, 231);
+ height: 100vh;
+
+ .container-inner {
+ margin-left: 30px;
+ margin-right: 30px;
+ padding: 20px;
+ background-color: white;
+ border: 1px solid #808080a6;
+ }
+}
+
+#first-name {
+ margin-right: 20px;
+ }
+
+.inputs {
+ flex-grow: 1;
+}
+
+.buttons {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+mat-form-field {
+ width: 100%;
+}
+
+::ng-deep label.mat-form-field-label {
+
+ span {
+ text-transform: capitalize !important;
+ }
+}
diff --git a/src/app/components/create-user/create-user.component.spec.ts b/src/app/components/create-user/create-user.component.spec.ts
new file mode 100644
index 0000000..825c94d
--- /dev/null
+++ b/src/app/components/create-user/create-user.component.spec.ts
@@ -0,0 +1,144 @@
+import { HttpClientModule } from '@angular/common/http';
+import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule, MatInputModule, MatPaginatorModule, MatSnackBar, MatSnackBarModule, MatTableModule } from '@angular/material';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { asyncScheduler, of } from 'rxjs';
+import { User } from 'src/app/models/user.model';
+import { UserRequest } from 'src/app/requests/user.request';
+import { UsersResponse } from 'src/app/responses/users.response';
+import { RoutingService } from 'src/app/services/routing.service';
+import { ApiResponse } from './../../interfaces/api-response.interface';
+import { UserService } from './../../services/user.service';
+import { CreateUserComponent } from './create-user.component';
+
+
+describe('CreateUserComponent', () => {
+ let component: CreateUserComponent;
+ let fixture: ComponentFixture;
+
+ let mockUserService: jasmine.SpyObj = jasmine.createSpyObj('UserService', ['getUsers', 'postUser', 'getByUsername'])
+ let mockUserData: User[] = [];
+ let mockUserDataByUsername: UsersResponse;
+ const mockSnackbarMock = jasmine.createSpyObj(['open']);
+ const mockRoutingService = jasmine.createSpyObj(['navigateToCreateUser']);
+ const mockNewUser = new UserRequest();
+ const userCreatedResponse: ApiResponse = {
+ data: new User
+ };
+
+ beforeEach(() => {
+ mockUserService.getUsers.and.callFake(function () {
+ return of(mockUserData, asyncScheduler)
+ });
+ mockUserService.postUser.and.callFake(function (newUser: UserRequest) {
+ return of(userCreatedResponse)
+ });
+ mockUserService.getByUsername.and.callFake(function (userName: string) {
+ return of(mockUserDataByUsername, asyncScheduler)
+ });
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ReactiveFormsModule,
+ MatTableModule,
+ MatPaginatorModule,
+ MatFormFieldModule,
+ RouterTestingModule,
+ HttpClientModule,
+ MatSnackBarModule,
+ MatInputModule,
+ BrowserAnimationsModule
+ ],
+ declarations: [CreateUserComponent],
+ providers: [{ provide: UserService, useValue: mockUserService }, { provide: MatSnackBar, useValue: mockSnackbarMock },
+ { provide: RoutingService, useValue: mockRoutingService }]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CreateUserComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+
+ it('it should add new user, display success toast', (fakeAsync(() => {
+ const addUserButton = fixture.debugElement.query(By.css('#btn-add-user'));
+ spyOn(component, 'addNewUser').and.callThrough();
+ spyOn(component, 'assignDataToNewUser').and.callFake(() => {
+ mockNewUser.first_name = 'DUMMY_first_name';
+ mockNewUser.last_name = 'DUMMY_last_name';
+ mockNewUser.username = 'DUMMY_username';
+ mockNewUser.email = 'DUMMY_email';
+ mockNewUser.id_status = 1;
+ component.newUser = mockNewUser;
+ });
+ mockUserDataByUsername = new UsersResponse();
+ mockUserService.getByUsername.and.returnValue(of(mockUserDataByUsername, asyncScheduler));
+ addUserButton.triggerEventHandler('click', null);
+ expect(component.addNewUser).toHaveBeenCalled();
+ tick(100);
+ expect(mockUserService.getByUsername).toHaveBeenCalled();
+ expect(component.assignDataToNewUser).toHaveBeenCalled();
+ expect(mockUserService.postUser).toHaveBeenCalledWith(component.newUser);
+ expect(component.newUser).toEqual(mockNewUser);
+ expect(mockSnackbarMock.open).toHaveBeenCalledWith("TOAST.SUCCESS.USER_CREATED", '', { duration: 10000, panelClass: 'success' });
+ }
+ )));
+
+ it('if user present, it should NOT add new user, display error toas', (fakeAsync(() => {
+ const addUserButton = fixture.debugElement.query(By.css('#btn-add-user'));
+ spyOn(component, 'addNewUser').and.callThrough();
+ spyOn(component, 'assignDataToNewUser');
+ mockUserDataByUsername = new UsersResponse();
+ mockUserDataByUsername.data.count = 1;
+ mockUserService.getByUsername.and.returnValue(of(mockUserDataByUsername, asyncScheduler));
+ addUserButton.triggerEventHandler('click', null);
+ expect(component.addNewUser).toHaveBeenCalled();
+ tick(100);
+ expect(mockUserService.getByUsername).toHaveBeenCalled();
+ expect(component.assignDataToNewUser).not.toHaveBeenCalled();
+ expect(component.newUser).toEqual(new UserRequest());
+ expect(mockSnackbarMock.open).toHaveBeenCalledWith("TOAST.ERROR.USERNAME_TAKEN", '', { duration: 10000, panelClass: 'warn' });
+ }
+ )));
+
+ it('it should assign data from form fields to user object', (fakeAsync(() => {
+ const userToSet = new UserRequest();
+ userToSet.first_name = 'DUMMY_firstName';
+ userToSet.last_name = 'DUMMY_lastName';
+ userToSet.username = 'DUMMY_userName';
+ userToSet.email = 'DUMMY_email@email.com';
+ userToSet.id_status = 1;
+
+ component.newUserForm.setValue({
+ firstName: userToSet.first_name,
+ lastName: userToSet.last_name,
+ userName: userToSet.username,
+ email: userToSet.email,
+ });
+ spyOn(component, 'addNewUser').and.callThrough();
+ spyOn(component, 'assignDataToNewUser').and.callThrough();
+ mockUserDataByUsername = new UsersResponse();
+ mockUserService.getByUsername.and.returnValue(of(mockUserDataByUsername, asyncScheduler));
+ const addUserButton = fixture.debugElement.query(By.css('#btn-add-user'));
+ addUserButton.triggerEventHandler('click', null);
+ expect(component.addNewUser).toHaveBeenCalled();
+ tick(100);
+ expect(mockUserService.getByUsername).toHaveBeenCalled();
+ expect(component.assignDataToNewUser).toHaveBeenCalled();
+ expect(component.newUser).toEqual(userToSet);
+ }
+ )));
+
+});
diff --git a/src/app/components/create-user/create-user.component.ts b/src/app/components/create-user/create-user.component.ts
new file mode 100644
index 0000000..f0c36ab
--- /dev/null
+++ b/src/app/components/create-user/create-user.component.ts
@@ -0,0 +1,71 @@
+import { MatSnackBar } from '@angular/material';
+import { Component } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { emailValidator } from 'src/app/validators/email.validator';
+import { userNameValidator } from 'src/app/validators/user-name-validator';
+import { UserRequest } from './../../requests/user.request';
+import { UsersResponse } from './../../responses/users.response';
+import { RoutingService } from './../../services/routing.service';
+import { UserService } from './../../services/user.service';
+
+@Component({
+ selector: 'exads-create-user',
+ templateUrl: './create-user.component.html',
+ styleUrls: ['./create-user.component.scss']
+})
+export class CreateUserComponent {
+
+ public newUser: UserRequest = new UserRequest();
+ public newUserForm: FormGroup = new FormGroup({
+ firstName: new FormControl('', [Validators.required]),
+ lastName: new FormControl('', []),
+ userName: new FormControl('', [Validators.required, Validators.maxLength(20), Validators.minLength(3), userNameValidator()]),
+ email: new FormControl('', [Validators.required, emailValidator()])
+ });;
+
+ constructor(private userService: UserService, private routingService: RoutingService,
+ private snackBar: MatSnackBar) { }
+
+ public cancel(): void {
+ this.routingService.navigateToUsersTable();
+ }
+
+ public addNewUser(): void {
+ this.userService.getByUsername(this.newUserForm.get("userName").value).toPromise().then((result: UsersResponse) => {
+ if (result && result.data && result.data.count > 0) {
+ this.snackBar.open("TOAST.ERROR.USERNAME_TAKEN", '', { duration: 10000 , panelClass: 'warn'});
+ } else if(result && result.data && result.data.count === 0) {
+ this.assignDataToNewUser();
+ this.postUser();
+ } else {
+ this.snackBar.open('TOAST.ERROR.UNKOWN', '', { duration: 10000, panelClass: 'error' });
+ console.error("something went wrong");
+ }
+ }).catch((err: Error) => {
+ console.error(err)
+ this.snackBar.open(err.message, '', { duration: 10000, panelClass: 'error' });
+ });
+ }
+
+ public assignDataToNewUser(): void {
+ this.newUser.first_name = this.newUserForm.get("firstName").value;
+ this.newUser.last_name = this.newUserForm.get("lastName").value;
+ this.newUser.username = this.newUserForm.get("userName").value;
+ this.newUser.email = this.newUserForm.get("email").value;
+ this.newUser.id_status = 1;
+ }
+
+ private postUser() {
+ this.userService.postUser(this.newUser).toPromise().then(() => {
+ this.snackBar.open("TOAST.SUCCESS.USER_CREATED", '', { duration: 10000 , panelClass: 'success'});
+ },
+ (err: Error) => {
+ console.error(err);
+ this.snackBar.open("TOAST.ERROR.REQUEST_FAILED", '', { duration: 10000, panelClass: 'error' });
+ });
+ }
+
+ public checkForError(controlName: string, errorName: string): boolean {
+ return this.newUserForm.controls[controlName].hasError(errorName);
+ }
+}
diff --git a/src/app/components/users-table/users-table.component.html b/src/app/components/users-table/users-table.component.html
new file mode 100644
index 0000000..a4d077f
--- /dev/null
+++ b/src/app/components/users-table/users-table.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ USERS.TABLE.USERNAME
+
+ {{element.username}}
+
+
+
+
+ USERS.TABLE.FULLNAME
+
+ {{element.first_name}} {{element.last_name}}
+
+
+
+
+
+ USERS.TABLE.EMAIL
+
+
+ {{element.email}}
+
+
+
+
+
+ USERS.TABLE.STATUS
+
+
+
+ {{element.id_status == 1 ? 'USERS.ACTIVE' : element.id_status == 3 ? 'USERS.DISABLED' : ''}}
+
+
+
+
+
+
+ USERS.TABLE.CREATED_DATE
+
+
+ {{element.created_date | date:'yyyy-mm-dd'}}
+
+
+
+
+
+
+
+
diff --git a/src/app/components/users-table/users-table.component.scss b/src/app/components/users-table/users-table.component.scss
new file mode 100644
index 0000000..83731c3
--- /dev/null
+++ b/src/app/components/users-table/users-table.component.scss
@@ -0,0 +1,34 @@
+.active {
+ color: green;
+}
+
+.disabled {
+ color: red;
+}
+
+.mat-row {
+ min-height: 20px;
+}
+.mat-row:nth-child(even) {
+ background-color: #f1f1f1;
+}
+
+::ng-deep #top-paginator {
+
+ .mat-paginator-page-size {
+ display: flex;
+ flex-direction: row-reverse;
+ }
+
+ .mat-paginator-range-actions {
+ position: absolute;
+ bottom: calc(100vh - 713px);
+ width: 100%;
+
+ .mat-paginator-range-label {
+ text-align: left;
+ width: 100%;
+ }
+ }
+}
+
diff --git a/src/app/components/users-table/users-table.component.spec.ts b/src/app/components/users-table/users-table.component.spec.ts
new file mode 100644
index 0000000..0d73b17
--- /dev/null
+++ b/src/app/components/users-table/users-table.component.spec.ts
@@ -0,0 +1,136 @@
+import { RoutingService } from './../../services/routing.service';
+import { CreateUserComponent } from './../create-user/create-user.component';
+import {
+ HttpClientTestingModule
+} from '@angular/common/http/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatInputModule, MatPaginatorModule, MatSnackBar, MatSnackBarModule, MatTableModule, PageEvent } from '@angular/material';
+import { MatButtonModule } from '@angular/material/button';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { asyncScheduler, of, throwError } from 'rxjs';
+import { User } from 'src/app/models/user.model';
+import { UsersResponse } from 'src/app/responses/users.response';
+import { UserRequest } from './../../requests/user.request';
+import { UserService } from './../../services/user.service';
+import { UsersTableComponent } from './users-table.component';
+import { fakeAsync, tick, discardPeriodicTasks } from '@angular/core/testing';
+
+
+describe('UsersTableComponent', () => {
+ let component: UsersTableComponent;
+ let fixture: ComponentFixture;
+ let app: UsersTableComponent;
+ let mockUserService: jasmine.SpyObj = jasmine.createSpyObj('UserService', ['getUsers', 'postUser', 'getByUsername'])
+ let mockUserData: User[] = [];
+ let mockUserDataByUsername: UsersResponse;
+ const mockSnackbarMock = jasmine.createSpyObj(['open']);
+ const mockRoutingService = jasmine.createSpyObj(['navigateToCreateUser']);
+
+ beforeEach(() => {
+ mockUserService.getUsers.and.callFake(function () {
+ return of(mockUserData, asyncScheduler)
+ });
+ mockUserService.postUser.and.callFake(function (newUser: UserRequest) {
+ return of(undefined)
+ });
+ mockUserService.getByUsername.and.callFake(function (userName: string) {
+ return of(mockUserDataByUsername, asyncScheduler)
+ });
+ });
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ MatSnackBarModule,
+ MatInputModule, MatPaginatorModule, MatTableModule, MatButtonModule],
+ declarations: [UsersTableComponent],
+ providers: [{ provide: UserService, useValue: mockUserService }, { provide: MatSnackBar, useValue: mockSnackbarMock },
+ { provide: RoutingService, useValue: mockRoutingService }]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UsersTableComponent);
+ component = fixture.componentInstance;
+ app = fixture.debugElement.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('it should assign a paginator', () => {
+ app.ngOnInit();
+ app.ngAfterViewInit();
+ expect(app.dataSource.paginator).toEqual(app.paginator);
+ });
+
+ it('it should retrieve users', async () => {
+ mockUserData = [
+ new User(),
+ new User(),
+ new User()
+ ];
+ mockUserData.forEach((user: User) => {
+ user.first_name = 'DUMMY_first_name'
+ user.last_name = 'DUMMY_last_name'
+ user.username = 'DUMMY_username'
+ user.email = 'DUMMY_email'
+ user.id = 123
+ user.id_status = 'DUMMY_id_status'
+ user.created_date = new Date(Date.now());
+ })
+ app.ngOnInit();
+ expect(mockUserService.getUsers).toHaveBeenCalled();
+ await mockUserService.getUsers().toPromise().then(() => {
+ expect(app.dataSource.data).toEqual(mockUserData);
+ })
+ });
+
+ it('it should set isMobile', () => {
+ app.ngOnInit();
+ app.ngAfterViewInit();
+ expect(app.isMobile).toEqual(window.innerWidth <= 600);
+ });
+
+ it('it should show error on getUsers failure', async () => {
+ mockUserService.getUsers.and.callFake(function () {
+ return throwError(new Error('error'), asyncScheduler);
+ });
+ app.ngOnInit();
+ expect(mockUserService.getUsers).toHaveBeenCalled();
+ await mockUserService.getUsers().subscribe(() => { }, () => {
+ expect(app.dataSource.data).toEqual([]);
+ expect(mockSnackbarMock.open).toHaveBeenCalledWith('error', '', { duration: 10000, panelClass: 'error' });
+ })
+ });
+
+ it('it should navigate to create user page', async () => {
+ const navigateButton = fixture.debugElement.query(By.css('#btn-nav-create-user'));
+ spyOn(app, 'navigateToCreateUser').and.callThrough();
+ navigateButton.triggerEventHandler('click', null);
+ expect(app.navigateToCreateUser).toHaveBeenCalled();
+ expect(mockRoutingService.navigateToCreateUser).toHaveBeenCalled();
+ });
+
+
+ it('it should adjust paginator position on page event', fakeAsync(() => {
+ spyOn(app, 'handlePageEvent').and.callThrough();
+ var calculateFunction = spyOn(app, 'calculateNewBottom').and.callThrough();
+ const pageEvent = new PageEvent();
+ pageEvent.pageSize = 10;
+ app.paginator.page.emit(pageEvent);
+ expect(app.handlePageEvent).toHaveBeenCalled();
+ tick(100);
+ expect(calculateFunction).toHaveBeenCalled();
+ const rangeActions = fixture.debugElement.query(By.css('.mat-paginator-range-actions'))
+ expect(rangeActions.nativeElement.style.bottom).toEqual((window.innerHeight - (app.userTableContainer.nativeElement.offsetHeight + 64 + 40)) + 'px');
+ }));
+
+});
diff --git a/src/app/components/users-table/users-table.component.ts b/src/app/components/users-table/users-table.component.ts
new file mode 100644
index 0000000..b2f3565
--- /dev/null
+++ b/src/app/components/users-table/users-table.component.ts
@@ -0,0 +1,59 @@
+import { AfterContentChecked, AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
+import { MatPaginator, MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material';
+import { User } from 'src/app/models/user.model';
+import { RoutingService } from './../../services/routing.service';
+import { UserService } from './../../services/user.service';
+
+@Component({
+ selector: 'exads-users-table',
+ templateUrl: './users-table.component.html',
+ styleUrls: ['./users-table.component.scss']
+})
+export class UsersTableComponent implements OnInit, AfterViewInit, AfterContentChecked {
+
+ public readonly fullScreenColumns = ['username', 'fullname', 'email', 'status', 'created'];
+ public readonly mobileColumns = ['username', 'fullname'];
+ public readonly pageSizes = [5, 10, 20];
+ public isMobile: boolean;
+ public dataSource = new MatTableDataSource();
+ @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator;
+
+ @ViewChild('userTableContainer', { static: false }) userTableContainer: ElementRef;
+ constructor(private userService: UserService,
+ private routingService: RoutingService, private snackBar: MatSnackBar) { }
+
+ ngAfterViewInit(): void {
+ this.dataSource.paginator = this.paginator;
+ }
+
+ ngOnInit(): void {
+ this.userService.getUsers().toPromise().then((users: User[]) => {
+ this.dataSource.data = users;
+ }).catch((err: Error) => {
+ this.snackBar.open(err.message, '', { duration: 10000, panelClass: 'error' });
+ console.error(err);
+ });
+ }
+
+ ngAfterContentChecked(): void {
+ this.isMobile = window.innerWidth <= 600;
+ }
+
+ public navigateToCreateUser(): void {
+ this.routingService.navigateToCreateUser();
+ }
+
+ public handlePageEvent(event: PageEvent) {
+ if (event && !!event.pageSize) {
+ setTimeout(() => {
+ const matPaginatorBottom: Element = document.querySelector('.mat-paginator-range-actions');
+ //@ts-ignore
+ matPaginatorBottom.style.bottom = this.calculateNewBottom(event.pageSize);
+ }, 60)
+ }
+ }
+
+ private calculateNewBottom() {
+ return (window.innerHeight - (this.userTableContainer.nativeElement.offsetHeight + 64 + 40)) + 'px'
+ }
+}
diff --git a/src/app/enums/api-routes.enum.ts b/src/app/enums/api-routes.enum.ts
new file mode 100644
index 0000000..b5cf8fa
--- /dev/null
+++ b/src/app/enums/api-routes.enum.ts
@@ -0,0 +1,4 @@
+export enum API_ROUTES {
+ USERS = 'users',
+ STATUSES = 'statuses'
+}
diff --git a/src/app/enums/app-routes.enum.ts b/src/app/enums/app-routes.enum.ts
new file mode 100644
index 0000000..3e8fd5c
--- /dev/null
+++ b/src/app/enums/app-routes.enum.ts
@@ -0,0 +1,4 @@
+export enum APP_ROUTES {
+ USERS = 'users',
+ CREATE_USER = 'users/create'
+}
diff --git a/src/app/interceptors/loading.interceptor.ts b/src/app/interceptors/loading.interceptor.ts
new file mode 100644
index 0000000..acab858
--- /dev/null
+++ b/src/app/interceptors/loading.interceptor.ts
@@ -0,0 +1,26 @@
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { LoadingService } from './../services/loading.service';
+
+
+@Injectable()
+export class LoadingInterceptor implements HttpInterceptor {
+
+ constructor(private loadingService: LoadingService) { }
+
+ intercept(req: HttpRequest, next: HttpHandler): Observable> {
+ this.loadingService.setLoading(true);
+ return next.handle(req)
+ .pipe(
+ map((event) => {
+ if(event && event.hasOwnProperty('url')){
+ this.loadingService.setLoading(false)
+ }
+ return event;
+ })
+ );
+
+ }
+}
diff --git a/src/app/interfaces/api-response.interface.ts b/src/app/interfaces/api-response.interface.ts
new file mode 100644
index 0000000..8ad58db
--- /dev/null
+++ b/src/app/interfaces/api-response.interface.ts
@@ -0,0 +1,5 @@
+export interface ApiResponse {
+ message?: string;
+ missingParams?: [];
+ data: T | { count?: number};
+}
diff --git a/src/app/interfaces/user.interface.ts b/src/app/interfaces/user.interface.ts
new file mode 100644
index 0000000..f2fd4a7
--- /dev/null
+++ b/src/app/interfaces/user.interface.ts
@@ -0,0 +1,7 @@
+export interface IUser {
+ first_name: string;
+ last_name: string;
+ email: string;
+ username: string;
+ id_status: string;
+}
diff --git a/src/app/mat-helpers/custom-mat-pager-intl.ts b/src/app/mat-helpers/custom-mat-pager-intl.ts
new file mode 100644
index 0000000..6b93522
--- /dev/null
+++ b/src/app/mat-helpers/custom-mat-pager-intl.ts
@@ -0,0 +1,16 @@
+import { Injectable } from "@angular/core";
+import { MatPaginatorIntl } from "@angular/material";
+
+@Injectable()
+export class CustomMatPagerIntl extends MatPaginatorIntl {
+ itemsPerPageLabel = 'MAT.PAGINATOR.ITEMS_PER_PAGE';
+ nextPageLabel = 'MAT.PAGINATOR.NEXT';
+ previousPageLabel = 'MAT.PAGINATOR.PREV';
+ showingLabel = 'MAT.PAGINATOR.SHOWING';
+ entriesLabel = 'MAT.PAGINATOR.SHOWING';
+
+ getRangeLabel = (page: number, pageSize: number, length: number) => {
+ // showing 1 - 16 / 16 entires
+ return this.showingLabel.concat(" ", page.toString(), " - ", pageSize.toString(), "/", length.toString(), " ", this.entriesLabel);
+ };
+}
diff --git a/src/app/models/status.model.ts b/src/app/models/status.model.ts
new file mode 100644
index 0000000..e6d959e
--- /dev/null
+++ b/src/app/models/status.model.ts
@@ -0,0 +1,3 @@
+export class Status {
+
+}
diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts
new file mode 100644
index 0000000..e1010b1
--- /dev/null
+++ b/src/app/models/user.model.ts
@@ -0,0 +1,10 @@
+import { IUser } from 'src/app/interfaces/user.interface';
+export class User implements IUser {
+ public id: number;
+ public first_name: string;
+ public last_name: string;
+ public email: string;
+ public username: string;
+ public created_date: Date;
+ public id_status: string;
+}
diff --git a/src/app/requests/user.request.ts b/src/app/requests/user.request.ts
new file mode 100644
index 0000000..f7f8d6e
--- /dev/null
+++ b/src/app/requests/user.request.ts
@@ -0,0 +1,7 @@
+export class UserRequest {
+ public first_name: string;
+ public last_name: string;
+ public email: string;
+ public username: string;
+ public id_status: number;
+}
diff --git a/src/app/responses/users.response.ts b/src/app/responses/users.response.ts
new file mode 100644
index 0000000..43feda9
--- /dev/null
+++ b/src/app/responses/users.response.ts
@@ -0,0 +1,15 @@
+import { User } from '../models/user.model';
+import { ApiResponse } from './../interfaces/api-response.interface';
+export class UsersResponse implements ApiResponse {
+
+ constructor(){
+ this.data = {
+ count: 0,
+ users: []
+ }
+ }
+ public data: {
+ count: number;
+ users: User[];
+ };
+}
diff --git a/src/app/services/loading.service.spec.ts b/src/app/services/loading.service.spec.ts
new file mode 100644
index 0000000..b6518ad
--- /dev/null
+++ b/src/app/services/loading.service.spec.ts
@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { LoadingService } from './loading.service';
+
+describe('LoadingService', () => {
+ beforeEach(() => TestBed.configureTestingModule({}));
+
+ it('should be created', () => {
+ const service: LoadingService = TestBed.get(LoadingService);
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/loading.service.ts b/src/app/services/loading.service.ts
new file mode 100644
index 0000000..aca9e0c
--- /dev/null
+++ b/src/app/services/loading.service.ts
@@ -0,0 +1,24 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class LoadingService {
+
+ private _isLoading: BehaviorSubject = new BehaviorSubject(false);
+
+ constructor() { }
+
+ public isLoading(): boolean {
+ return this._isLoading.getValue();
+ }
+
+ public setLoading(loading: boolean){
+ this._isLoading.next(loading);
+ }
+
+ public loading(): Observable {
+ return this._isLoading.asObservable();
+ }
+}
diff --git a/src/app/services/routing.service.spec.ts b/src/app/services/routing.service.spec.ts
new file mode 100644
index 0000000..34d8633
--- /dev/null
+++ b/src/app/services/routing.service.spec.ts
@@ -0,0 +1,15 @@
+import { RouterTestingModule } from '@angular/router/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { RoutingService } from './routing.service';
+
+describe('RoutingService', () => {
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [RouterTestingModule]
+ }));
+
+ it('should be created', () => {
+ const service: RoutingService = TestBed.get(RoutingService);
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/routing.service.ts b/src/app/services/routing.service.ts
new file mode 100644
index 0000000..e257124
--- /dev/null
+++ b/src/app/services/routing.service.ts
@@ -0,0 +1,20 @@
+import { APP_ROUTES } from '../enums/app-routes.enum';
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class RoutingService {
+
+ constructor(private router: Router) { }
+
+ public navigateToCreateUser(): void {
+ this.router.navigateByUrl(APP_ROUTES.CREATE_USER);
+ }
+
+ public navigateToUsersTable(): void {
+ this.router.navigateByUrl(APP_ROUTES.USERS);
+ }
+
+}
diff --git a/src/app/services/user.service.spec.ts b/src/app/services/user.service.spec.ts
new file mode 100644
index 0000000..e0bce59
--- /dev/null
+++ b/src/app/services/user.service.spec.ts
@@ -0,0 +1,15 @@
+import { HttpClientModule } from '@angular/common/http';
+import { TestBed } from '@angular/core/testing';
+
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [HttpClientModule]
+ }));
+
+ it('should be created', () => {
+ const service: UserService = TestBed.get(UserService);
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts
new file mode 100644
index 0000000..4c38a85
--- /dev/null
+++ b/src/app/services/user.service.ts
@@ -0,0 +1,33 @@
+import { UserRequest } from './../requests/user.request';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { environment } from 'src/environments/environment';
+import { User } from '../models/user.model';
+import { API_ROUTES } from './../enums/api-routes.enum';
+import { UsersResponse } from './../responses/users.response';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class UserService {
+
+ constructor(private http: HttpClient) { }
+
+ public getUsers(): Observable {
+ return this.http.get(environment.apiBaseUrl + '/' + API_ROUTES.USERS)
+ .pipe(map((response: UsersResponse) => { return response.data.users }));
+ }
+
+ public postUser(newUser: UserRequest): Observable {
+ return this.http.post(environment.apiBaseUrl + '/' + API_ROUTES.USERS, {user: newUser});
+ }
+
+ public getByUsername(username: string): Observable {
+ let queryParams = new HttpParams();
+ queryParams = queryParams.append("username", username);
+ return this.http.get(environment.apiBaseUrl + '/' + API_ROUTES.USERS,{params:queryParams})
+ }
+}
+
diff --git a/src/app/validators/email.validator.ts b/src/app/validators/email.validator.ts
new file mode 100644
index 0000000..d9e4dcc
--- /dev/null
+++ b/src/app/validators/email.validator.ts
@@ -0,0 +1,8 @@
+import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
+
+export function emailValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const email = control.value;
+ return !email || /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b/.test(email) ? null: { invalidEmail: true };
+ }
+}
diff --git a/src/app/validators/user-name-validator.ts b/src/app/validators/user-name-validator.ts
new file mode 100644
index 0000000..9b1a375
--- /dev/null
+++ b/src/app/validators/user-name-validator.ts
@@ -0,0 +1,11 @@
+import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
+
+export function userNameValidator(): ValidatorFn {
+ return (control: AbstractControl): ValidationErrors | null => {
+ const userName = control.value;
+ if (!userName) {
+ return null;
+ }
+ return /[(){}".![\]]+/.test(userName) ? { invalidCharacter: true } : null;
+ }
+}
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
index ffe8aed..60aaac8 100644
--- a/src/environments/environment.ts
+++ b/src/environments/environment.ts
@@ -1,3 +1,4 @@
export const environment = {
- production: false
+ production: false,
+ apiBaseUrl: 'http://localhost:3000',
};
diff --git a/src/global-styling/flex.scss b/src/global-styling/flex.scss
new file mode 100644
index 0000000..a1f632f
--- /dev/null
+++ b/src/global-styling/flex.scss
@@ -0,0 +1,24 @@
+
+.flex {
+ display: flex;
+
+ &.column {
+ flex-direction: column;
+ }
+
+ &.space-between {
+ justify-content: space-between;
+ }
+ // style overrides for "sm" breakpoint
+ .sm {
+ // breakpont
+ @media only screen and (max-width: $small) {
+ &-column {
+ flex-direction: column;
+ }
+ }
+
+ }
+}
+
+
diff --git a/src/global-styling/snackbar.scss b/src/global-styling/snackbar.scss
new file mode 100644
index 0000000..32a7eeb
--- /dev/null
+++ b/src/global-styling/snackbar.scss
@@ -0,0 +1,13 @@
+snack-bar-container {
+ &.success{
+ color: #35f817;
+ }
+
+ &.warn {
+ color: #f27500;
+ }
+
+ &.error {
+ background-color: #f32c1e;
+ }
+}
diff --git a/src/global-styling/variables.scss b/src/global-styling/variables.scss
new file mode 100644
index 0000000..8cad78f
--- /dev/null
+++ b/src/global-styling/variables.scss
@@ -0,0 +1,2 @@
+// sttyling breakpoints
+$small: 600px;
diff --git a/src/styles.scss b/src/styles.scss
index dda6814..4318327 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1,3 +1,10 @@
+// angular material theming
+@import '../node_modules/@swimlane/ngx-datatable/src/themes/material.scss';
+@import "~@angular/material/prebuilt-themes/indigo-pink.css";
+// project specific global stylings
+@import "global-styling/variables.scss";
+@import "global-styling/flex.scss";
+@import "global-styling/snackbar.scss";
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }