Login, Registration Forms Added.

This commit is contained in:
Charles Showalter 2022-05-23 15:30:07 -07:00
parent f8b7acd5c6
commit 2bca44ebe5
16 changed files with 265 additions and 30 deletions

View File

@ -1,20 +1,38 @@
import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, map } from 'rxjs';
import { map, of, ReplaySubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { IUser } from '../shared/models/user';
@Injectable({
providedIn: 'root'
})
export class AccoutService {
export class AccountService {
baseUrl = environment.apiUrl;
private currentUserSource = new BehaviorSubject<IUser>(null);
private currentUserSource = new ReplaySubject<IUser>(1);
currentUser$ = this.currentUserSource.asObservable();
constructor(private http: HttpClient, private router: Router) { }
loadCurrentUser(token: string){
if(token === null){
this.currentUserSource.next(null);
return of(null);
}
let headers = new HttpHeaders();
headers = headers.set('Authorization', `Bearer ${token}`);
return this.http.get(this.baseUrl + 'account', {headers}).pipe(
map((user: IUser) => {
if(user){
localStorage.setItem('token', user.token);
this.currentUserSource.next(user);
}
})
)
}
login(values: any){
return this.http.post(this.baseUrl + 'account/login', values).pipe(
map((user: IUser) =>{
@ -31,6 +49,7 @@ export class AccoutService {
map((user:IUser) => {
if(user){
localStorage.setItem('token', user.token);
this.currentUserSource.next(user);
}
})
);
@ -43,6 +62,6 @@ export class AccoutService {
}
checkEmailExists(email: string){
return this.http.get(this.baseUrl + '/account/emailexists?email=' + email);
return this.http.get(this.baseUrl + 'account/emailexists?email=' + email);
}
}

View File

@ -1,17 +1,10 @@
<div class="d-flex justify-content-center mt-5">
<div class="col-3">
<form>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<h1 class="h3 mb-3 fw-normal text-center">Login</h1>
<div class="form-floating mb-2">
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-2">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
<app-text-inputs formControlName="email" [label]="'Email Address'"></app-text-inputs>
<app-text-inputs formControlName="password" [label]="'Password'" [type]="'password'"></app-text-inputs>
<button [disabled]="loginForm.invalid" class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
</form>
</div>
</div>

View File

@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { AccountService } from '../account.service';
@Component({
selector: 'app-login',
@ -8,18 +10,30 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;
returnUrl: string;
constructor() { }
constructor(private accountService: AccountService, private router: Router, private activatedRoute: ActivatedRoute) { }
ngOnInit(): void {
this.returnUrl = this.activatedRoute.snapshot.queryParams.returnUrl || '/shop';
this.createLoginForm();
}
createLoginForm(){
this.loginForm = new FormGroup({
email: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.pattern('^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}')]),
password: new FormControl('', Validators.required)
});
}
onSubmit(){
this.accountService.login(this.loginForm.value).subscribe({
next: () => { this.router.navigateByUrl(this.returnUrl); },
error: (e: any) => { console.log(e) },
complete: () => { console.log('complete')}
});
}
}

View File

@ -1 +1,17 @@
<p>register works!</p>
<div class="d-flex justify-content-center mt-5">
<div class="col-3">
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<h1 class="h3 mb-3 fw-normal text-center">Register</h1>
<app-text-inputs formControlName="displayName" [label]="'Display Name'"></app-text-inputs>
<app-text-inputs formControlName="email" [label]="'Email Address'"></app-text-inputs>
<app-text-inputs formControlName="password" [label]="'Password'" [type]="'password'"></app-text-inputs>
<ul class="text-warning list-unstyled" *ngIf="errors">
<li *ngFor="let error of errors">
{{error}}
</li>
</ul>
<button [disabled]="registerForm.invalid" class="w-100 btn btn-lg btn-primary" type="submit">Register</button>
</form>
</div>
</div>

View File

@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { AsyncValidatorFn, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { map, of, switchMap, timer } from 'rxjs';
import { AccountService } from '../account.service';
@Component({
selector: 'app-register',
@ -6,10 +10,52 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
registerForm: FormGroup;
errors: string[];
constructor() { }
constructor(private fb: FormBuilder, private accountService: AccountService, private router: Router) { }
ngOnInit(): void {
this.createRegisterForm();
}
createRegisterForm(){
this.registerForm = this.fb.group({
displayName: [null, [Validators.required]],
email: [null,
[Validators.required, Validators.pattern('^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}')],
[this.validateEmailNotTaken()]
],
password: [null, [Validators.required],]
});
}
onSubmit(){
this.accountService.register(this.registerForm.value).subscribe({
next: (response) => { this.router.navigateByUrl('/shop')},
error: (e: any) => {
console.log(e);
this.errors = e.errors;
},
complete: () => { console.log('completed') }
});
}
validateEmailNotTaken(): AsyncValidatorFn {
return control => {
return timer(500).pipe(
switchMap(() => {
if(!control.value){
return of(null);
}
return this.accountService.checkEmailExists(control.value).pipe(
map(res => {
return res ? {emailExists: true} : null;
})
);
})
);
}
}
}

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { NotFoundComponent } from './core/not-found/not-found.component';
import { ServerErrorComponent } from './core/server-error/server-error.component';
import { TestErrorComponent } from './core/test-error/test-error.component';
@ -12,7 +13,7 @@ const routes: Routes = [
{path: 'not-found', component: NotFoundComponent, data: {breadcrumb: 'Not Found'}},
{path: 'shop', loadChildren: ()=> import('./shop/shop.module').then(mod => mod.ShopModule), data: {breadcrumb: 'Shop'}},
{path: 'basket', loadChildren: ()=> import('./basket/basket.module').then(mod => mod.BasketModule), data: {breadcrumb: 'Shopping Cart'}},
{path: 'checkout', loadChildren: ()=> import('./checkout/checkout.module').then(mod => mod.CheckoutModule), data: {breadcrumb: 'Checkout'}},
{path: 'checkout', canActivate: [AuthGuard], loadChildren: ()=> import('./checkout/checkout.module').then(mod => mod.CheckoutModule), data: {breadcrumb: 'Checkout'}},
{path: 'account', loadChildren: ()=> import('./account/account.module').then(mod => mod.AccountModule), data: {breadcrumb: {skip: true}}},
{path: '**', redirectTo: 'not-found', pathMatch: 'full'}
];

View File

@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { BasketService } from './basket/basket.service';
import { AccountService } from './account/account.service';
@Component({
selector: 'app-root',
@ -9,9 +10,23 @@ import { BasketService } from './basket/basket.service';
export class AppComponent implements OnInit {
title = 'SkiNet';
constructor(private basketService: BasketService) {}
constructor(private basketService: BasketService, private accountService: AccountService) {}
ngOnInit(): void {
this.loadBasket();
this.loadCurrentUser();
}
loadCurrentUser(){
const token = localStorage.getItem('token');
this.accountService.loadCurrentUser(token).subscribe({
next: () => { console.log('loader user') },
error: (e: any) => { console.log(e) },
complete: () => { console.log('completed') }
});
}
loadBasket(){
const basketId = localStorage.getItem('basket_id');
if(basketId){
this.basketService.getBasket(basketId).subscribe({

View File

@ -8,6 +8,7 @@ import { ServerErrorComponent } from './server-error/server-error.component';
import { ToastrModule } from 'ngx-toastr';
import { SectionHeaderComponent } from './section-header/section-header.component';
import { BreadcrumbModule } from 'xng-breadcrumb';
import { SharedModule } from '../shared/shared.module';
@ -17,6 +18,7 @@ import { BreadcrumbModule } from 'xng-breadcrumb';
CommonModule,
RouterModule,
BreadcrumbModule,
SharedModule,
ToastrModule.forRoot({
positionClass: 'toast-bottom-right',
preventDuplicates: true

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { map, Observable } from 'rxjs';
import { AccountService } from 'src/app/account/account.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private accountService: AccountService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.accountService.currentUser$.pipe(
map(auth => {
if(auth){
return true;
}
this.router.navigate(['account/login'], {queryParams: {returnUrl: state.url}});
})
);
}
}

View File

@ -8,7 +8,10 @@ export class LoadingInterceptor implements HttpInterceptor {
constructor(private busyService: BusyService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.busyService.busy();
if(!req.url.includes('emailexists')){
this.busyService.busy();
}
return next.handle(req).pipe(
delay(1000),
finalize(()=> {

View File

@ -10,8 +10,29 @@
<i class="fa fa-shopping-cart fa-2x me-3 text-dark"></i>
<div *ngIf="(basket$ | async) as basket" class="cart-no">{{basket.items.length}}</div>
</a>
<a routerLink="/account/login" class="btn btn-outline-secondary me-3" href="#">Login</a>
<a routerLink="/account/register" class="btn btn-outline-secondary me-3" href="#">Sign up</a>
<ng-container *ngIf="(currentUser$ | async) === null">
<a routerLink="/account/login" class="btn btn-outline-secondary ms-3 me-3" href="#">Login</a>
<a routerLink="/account/register" class="btn btn-outline-secondary me-3" href="#">Sign up</a>
</ng-container>
<ng-container *ngIf="(currentUser$ | async) as user">
<div class="dropdown ms-3 me-5" dropdown>
<a class="dropdown-toggle" style="cursor: pointer" dropdownToggle>
<strong>Welcome {{(user.displayName)}}</strong>
</a>
<div class="dropdown-menu dropdown-menu-right" style="cursor: pointer" *dropdownMenu>
<a routerLink="/basket" class="dropdown-item d-flex align-items-center py-2">
<i class="fa fa-shopping-cart me-3"></i> View Basket
</a>
<a routerLink="/orders" class="dropdown-item d-flex align-items-center py-2">
<i class="fa fa-history me-3"></i> View Orders
</a>
<div class="dropdown-divider"></div>
<a (click)="logout()" class="dropdown-item d-flex align-items-center py-2">
<i class="fa fa-sign-out me-3"></i> Logout
</a>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { AccountService } from 'src/app/account/account.service';
import { BasketService } from 'src/app/basket/basket.service';
import { IBasket } from 'src/app/shared/models/baskset';
import { IUser } from 'src/app/shared/models/user';
@Component({
selector: 'app-nav-bar',
@ -9,12 +11,18 @@ import { IBasket } from 'src/app/shared/models/baskset';
styleUrls: ['./nav-bar.component.scss']
})
export class NavBarComponent implements OnInit {
basket$ : Observable<IBasket>
basket$ : Observable<IBasket>;
currentUser$: Observable<IUser>;
constructor(private basketService: BasketService) { }
constructor(private basketService: BasketService, private accountService: AccountService) { }
ngOnInit(): void {
this.basket$ = this.basketService.basket$;
this.currentUser$ = this.accountService.currentUser$;
}
logout() {
this.accountService.logout();
}
}

View File

@ -0,0 +1,21 @@
<div class="form-floating mb-2">
<input
[ngClass]="(controlDir && controlDir.control && controlDir.control.touched) ? !controlDir.control.valid ? 'is-invalid' : 'is-valid' : null"
[type]="type"
(input)="onChange($event.target.value)"
(blur)="onTouched()"
class="form-control"
id="{{label}}"
#input
placeholder="{{label}}"
>
<div *ngIf="controlDir && controlDir.control && controlDir.control.status === 'PENDING'" class="fa fa-spinner fa-spin loader"></div>
<label for="{{label}}">{{label}}</label>
<div class="text-warning" *ngIf="(controlDir && controlDir.control && !controlDir.control.valid && controlDir.control.touched)">
<span *ngIf="controlDir.control.errors?.required">{{label}} is required</span>
<span *ngIf="controlDir.control.errors?.pattern">Email is invalid</span>
</div>
<div class="text-warning d-block" *ngIf="(controlDir && controlDir.control && !controlDir.control.valid && controlDir.control.dirty)">
<span *ngIf="controlDir.control.errors?.emailExists">Email Address is in use.</span>
</div>
</div>

View File

@ -0,0 +1,7 @@
.loader {
position: absolute;
width: auto;
top: 20px;
right: 10px;
margin-top: 0;
}

View File

@ -0,0 +1,40 @@
import { Component, ElementRef, Input, OnInit, Self, ViewChild } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
@Component({
selector: 'app-text-inputs',
templateUrl: './text-inputs.component.html',
styleUrls: ['./text-inputs.component.scss']
})
export class TextInputsComponent implements OnInit, ControlValueAccessor {
@ViewChild('input', {static: true}) input: ElementRef;
@Input() type = 'text';
@Input() label: string;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit(): void {
const control = this.controlDir.control;
const validators = control.validator ? [control.validator] : [];
const asyncValidators = control.asyncValidator ? [control.asyncValidator] : [];
control.setValidators(validators);
control.setAsyncValidators(asyncValidators);
control.updateValueAndValidity();
}
onChange(evt) {}
onTouched(evt?) {}
writeValue(obj: any): void {
this.input.nativeElement.value = obj || '';
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}

View File

@ -6,19 +6,22 @@ import { CarouselModule } from 'ngx-bootstrap/carousel';
import { PagingHeaderComponent } from './components/paging-header/paging-header.component';
import { PagerComponent } from './components/pager/pager.component';
import { OrderTotalsComponent } from './components/order-totals/order-totals.component';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { TextInputsComponent } from './components/text-inputs/text-inputs.component';
@NgModule({
declarations: [
PagingHeaderComponent,
PagerComponent,
OrderTotalsComponent
OrderTotalsComponent,
TextInputsComponent
],
imports: [
CommonModule,
PaginationModule.forRoot(),
CarouselModule.forRoot(),
BsDropdownModule.forRoot(),
ReactiveFormsModule
],
exports: [
@ -27,7 +30,9 @@ import { OrderTotalsComponent } from './components/order-totals/order-totals.com
PagerComponent,
CarouselModule,
OrderTotalsComponent,
ReactiveFormsModule
ReactiveFormsModule,
BsDropdownModule,
TextInputsComponent
]
})
export class SharedModule { }