28 changed files with 426 additions and 145 deletions
@ -1,30 +1,10 @@ |
|||
<mat-toolbar color="primary"> |
|||
<span>Phone Book</span> |
|||
<span class="spacer"></span> |
|||
<button *ngIf="authService.isLoggedIn" mat-icon-button aria-label="Logout" (click)="logout()"> |
|||
<mat-icon>logout</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div class="content-wrapper"> |
|||
<button class="new-record" (click)="newRecord()" mat-button> |
|||
<mat-icon>add</mat-icon> Add Record |
|||
</button> |
|||
<mat-form-field appearance="fill"> |
|||
<mat-icon matPrefix>search</mat-icon> |
|||
<mat-label>Search</mat-label> |
|||
<input matInput [value]="searchValue" (input)="search($event)" /> |
|||
</mat-form-field> |
|||
|
|||
<div class="container-wrapper"> |
|||
<div class="container"> |
|||
<mat-card> |
|||
<app-record |
|||
*ngFor="let record of filteredPhoneBook()" |
|||
[record]="record" |
|||
(edit)="editRecord($event)" |
|||
(delete)="deleteRecord($event)" |
|||
></app-record> |
|||
<p class="empty-label" *ngIf="filteredPhoneBook().length === 0"> |
|||
No results |
|||
</p> |
|||
</mat-card> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<router-outlet></router-outlet> |
|||
</div> |
|||
@ -0,0 +1,24 @@ |
|||
<button class="new-record" (click)="newRecord()" mat-button> |
|||
<mat-icon>add</mat-icon> Add Record |
|||
</button> |
|||
<mat-form-field appearance="fill"> |
|||
<mat-icon matPrefix>search</mat-icon> |
|||
<mat-label>Search</mat-label> |
|||
<input matInput [value]="searchValue" (input)="search($event)" /> |
|||
</mat-form-field> |
|||
|
|||
<div class="container-wrapper"> |
|||
<div class="container"> |
|||
<mat-card> |
|||
<app-record |
|||
*ngFor="let record of filteredPhoneBook()" |
|||
[record]="record" |
|||
(edit)="editRecord($event)" |
|||
(delete)="deleteRecord($event)" |
|||
></app-record> |
|||
<p class="empty-label" *ngIf="filteredPhoneBook().length === 0"> |
|||
No results |
|||
</p> |
|||
</mat-card> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,35 @@ |
|||
.content-wrapper { |
|||
max-width: 1400px; |
|||
margin: auto; |
|||
} |
|||
|
|||
.container-wrapper { |
|||
display: flex; |
|||
justify-content: space-around; |
|||
} |
|||
|
|||
.container { |
|||
width: 100%; |
|||
margin: 0 25px 25px 0; |
|||
} |
|||
|
|||
.list { |
|||
border: solid 1px #ccc; |
|||
min-height: 60px; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.new-record { |
|||
margin-bottom: 30px; |
|||
} |
|||
|
|||
.empty-label { |
|||
font-size: 2em; |
|||
padding-top: 10px; |
|||
text-align: center; |
|||
opacity: 0.2; |
|||
} |
|||
|
|||
.content-wrapper > * { |
|||
margin: 4px 4px 0px; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
import { ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { PhoneBookComponent } from './phone-book.component'; |
|||
|
|||
describe('PhoneBookComponent', () => { |
|||
let component: PhoneBookComponent; |
|||
let fixture: ComponentFixture<PhoneBookComponent>; |
|||
|
|||
beforeEach(async () => { |
|||
await TestBed.configureTestingModule({ |
|||
declarations: [ PhoneBookComponent ] |
|||
}) |
|||
.compileComponents(); |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(PhoneBookComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,88 @@ |
|||
import { Component } from '@angular/core'; |
|||
import { Record } from '../record/record'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { |
|||
RecordDialogComponent, |
|||
RecordDialogResult, |
|||
} from '../record-dialog/record-dialog.component'; |
|||
import { AngularFirestore } from '@angular/fire/compat/firestore'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'app-phone-book', |
|||
templateUrl: './phone-book.component.html', |
|||
styleUrls: ['./phone-book.component.less'], |
|||
}) |
|||
export class PhoneBookComponent { |
|||
phoneBook = this.store |
|||
.collection('phoneBook') |
|||
.valueChanges({ idField: 'id' }) as Observable<Record[]>; |
|||
searchValue = ''; |
|||
phoneBookLocal: Record[] = []; |
|||
phoneBookObserver = { |
|||
next: (x: Record[]) => { |
|||
this.phoneBookLocal = x; |
|||
}, |
|||
error: (err: String) => console.error('Observer got an error: ' + err), |
|||
complete: () => console.log('Observer got a complete notification'), |
|||
}; |
|||
|
|||
constructor(private dialog: MatDialog, private store: AngularFirestore) { |
|||
this.phoneBook.subscribe(this.phoneBookObserver); |
|||
} |
|||
|
|||
newRecord(): void { |
|||
const dialogRef = this.dialog.open(RecordDialogComponent, { |
|||
width: '270px', |
|||
data: { |
|||
record: {}, |
|||
}, |
|||
}); |
|||
dialogRef |
|||
.afterClosed() |
|||
.subscribe((result: RecordDialogResult | undefined) => { |
|||
if (!result) { |
|||
return; |
|||
} |
|||
this.store.collection('phoneBook').add(result.record); |
|||
}); |
|||
} |
|||
|
|||
editRecord(record: Record): void { |
|||
const dialogRef = this.dialog.open(RecordDialogComponent, { |
|||
width: '270px', |
|||
data: { |
|||
record: { |
|||
name: record.name, |
|||
phoneNumber: record.phoneNumber, |
|||
}, |
|||
}, |
|||
}); |
|||
dialogRef |
|||
.afterClosed() |
|||
.subscribe((result: RecordDialogResult | undefined) => { |
|||
if (!result) { |
|||
return; |
|||
} |
|||
this.store.collection('phoneBook').doc(record.id).update(result.record); |
|||
}); |
|||
} |
|||
|
|||
deleteRecord(record: Record): void { |
|||
this.store.collection('phoneBook').doc(record.id).delete(); |
|||
} |
|||
|
|||
search(event: Event): void { |
|||
this.searchValue = (event.target as HTMLInputElement).value; |
|||
} |
|||
|
|||
filteredPhoneBook(): Record[] { |
|||
if (this.searchValue == '') return this.phoneBookLocal; |
|||
|
|||
return this.phoneBookLocal.filter( |
|||
(p) => |
|||
p.name.includes(this.searchValue) || |
|||
p.phoneNumber.includes(this.searchValue) |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
export interface formData{ |
|||
email: string; |
|||
password: string; |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
<div class="container"> |
|||
<mat-card> |
|||
<mat-form-field appearance="fill"> |
|||
<mat-label>Email</mat-label> |
|||
<input matInput cdkFocusInitial [(ngModel)]="data.email" type="text" /> |
|||
</mat-form-field> |
|||
|
|||
<mat-form-field appearance="fill"> |
|||
<mat-label>Password</mat-label> |
|||
<input matInput [(ngModel)]="data.password" type="password" /> |
|||
</mat-form-field> |
|||
|
|||
<button mat-button (click)="login()">Sign in</button> |
|||
</mat-card> |
|||
</div> |
|||
@ -0,0 +1,9 @@ |
|||
:host { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
} |
|||
|
|||
mat-card > *{ |
|||
display: block; |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
import { ComponentFixture, TestBed } from '@angular/core/testing'; |
|||
|
|||
import { SignInComponent } from './sign-in.component'; |
|||
|
|||
describe('SignInComponent', () => { |
|||
let component: SignInComponent; |
|||
let fixture: ComponentFixture<SignInComponent>; |
|||
|
|||
beforeEach(async () => { |
|||
await TestBed.configureTestingModule({ |
|||
declarations: [ SignInComponent ] |
|||
}) |
|||
.compileComponents(); |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
fixture = TestBed.createComponent(SignInComponent); |
|||
component = fixture.componentInstance; |
|||
fixture.detectChanges(); |
|||
}); |
|||
|
|||
it('should create', () => { |
|||
expect(component).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,30 @@ |
|||
import { Component, OnInit, Inject } from '@angular/core'; |
|||
import { AuthService } from '../../shared/services/auth.service'; |
|||
import { formData } from './formData'; |
|||
|
|||
@Component({ |
|||
selector: 'app-sign-in', |
|||
templateUrl: './sign-in.component.html', |
|||
styleUrls: ['./sign-in.component.less'] |
|||
}) |
|||
export class SignInComponent implements OnInit { |
|||
|
|||
data: formData = { |
|||
email: "", |
|||
password: "" |
|||
}; |
|||
|
|||
|
|||
constructor(private authService: AuthService) {} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
login() { |
|||
this.authService.login( |
|||
this.data.email, |
|||
this.data.password |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { TestBed } from '@angular/core/testing'; |
|||
|
|||
import { AuthGuard } from './auth.guard'; |
|||
|
|||
describe('AuthGuard', () => { |
|||
let guard: AuthGuard; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({}); |
|||
guard = TestBed.inject(AuthGuard); |
|||
}); |
|||
|
|||
it('should be created', () => { |
|||
expect(guard).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,26 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; |
|||
import { Observable } from 'rxjs'; |
|||
import { AuthService } from '../services/auth.service'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root' |
|||
}) |
|||
export class AuthGuard implements CanActivate { |
|||
|
|||
constructor( |
|||
public authService: AuthService, |
|||
public router: Router |
|||
){ } |
|||
|
|||
|
|||
canActivate( |
|||
next: ActivatedRouteSnapshot, |
|||
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { |
|||
if(this.authService.isLoggedIn !== true) { |
|||
this.router.navigate(['sign-in']) |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { TestBed } from '@angular/core/testing'; |
|||
|
|||
import { AuthService } from './auth.service'; |
|||
|
|||
describe('AuthService', () => { |
|||
let service: AuthService; |
|||
|
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({}); |
|||
service = TestBed.inject(AuthService); |
|||
}); |
|||
|
|||
it('should be created', () => { |
|||
expect(service).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,69 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/compat/firestore'; |
|||
import { AngularFireAuth } from '@angular/fire/compat/auth'; |
|||
import * as firebase from 'firebase/app'; |
|||
import { User } from './user'; |
|||
|
|||
@Injectable() |
|||
export class AuthService { |
|||
|
|||
userData: any; |
|||
|
|||
constructor( |
|||
public afs: AngularFirestore, // Inject Firestore service
|
|||
public afAuth: AngularFireAuth, // Inject Firebase auth service
|
|||
public router: Router |
|||
) { |
|||
/* Saving user data in localstorage when |
|||
logged in and setting up null when logged out */ |
|||
this.afAuth.authState.subscribe(user => { |
|||
if (user) { |
|||
this.userData = user; |
|||
localStorage.setItem('user', JSON.stringify(this.userData)); |
|||
JSON.parse(localStorage.getItem('user') as string); |
|||
} else { |
|||
localStorage.setItem('user', null as any); |
|||
JSON.parse(localStorage.getItem('user') as string); |
|||
} |
|||
}) |
|||
} |
|||
|
|||
login(email : string, password : string) { |
|||
return this.afAuth.signInWithEmailAndPassword(email, password) |
|||
.then((result) => { |
|||
this.SetUserData(result.user as User); |
|||
localStorage.setItem('user', JSON.stringify(result.user)); |
|||
JSON.parse(localStorage.getItem('user') as string); |
|||
this.router.navigate(['']); |
|||
}).catch((error) => { |
|||
window.alert(error.message) |
|||
}) |
|||
} |
|||
|
|||
get isLoggedIn(): boolean { |
|||
const user = JSON.parse(localStorage.getItem('user') as string); |
|||
return user !== null; |
|||
} |
|||
|
|||
SetUserData(user: User) { |
|||
const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${user.uid}`); |
|||
const userData: User = { |
|||
uid: user.uid, |
|||
email: user.email, |
|||
displayName: user.displayName, |
|||
photoURL: user.photoURL, |
|||
emailVerified: user.emailVerified |
|||
} |
|||
return userRef.set(userData, { |
|||
merge: true |
|||
}) |
|||
} |
|||
|
|||
logout() { |
|||
return this.afAuth.signOut().then(() => { |
|||
localStorage.removeItem('user'); |
|||
this.router.navigate(['sign-in']); |
|||
}) |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
export interface User { |
|||
uid: string; |
|||
email: string; |
|||
displayName: string; |
|||
photoURL: string; |
|||
emailVerified: boolean; |
|||
} |
|||
Loading…
Reference in new issue