28 changed files with 426 additions and 145 deletions
@ -1,43 +1,11 @@ |
|||||
mat-toolbar { |
mat-toolbar { |
||||
margin-bottom: 20px; |
margin-bottom: 20px; |
||||
} |
} |
||||
|
|
||||
mat-toolbar > span { |
mat-toolbar > span { |
||||
margin-left: 10px; |
margin-left: 10px; |
||||
} |
} |
||||
|
|
||||
.content-wrapper { |
.spacer { |
||||
max-width: 1400px; |
flex: 1 1 auto; |
||||
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,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