From 015972e9133f3c2266ef1a1b4c45d0083cc98b7a Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sat, 15 Jul 2017 14:07:02 +0100 Subject: [PATCH 01/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=201:=20Firebase?= =?UTF-8?q?=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .firebaserc | 5 +++++ database.rules.json | 31 +++++++++++++++++++++++++++++++ firebase.json | 30 ++++++++++++++++++++++++++++++ src/app/app.module.ts | 11 +++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .firebaserc create mode 100644 database.rules.json create mode 100644 firebase.json diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..7626514d --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "fitness-app-e668a" + } +} diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 00000000..f36a9ddd --- /dev/null +++ b/database.rules.json @@ -0,0 +1,31 @@ +{ + "rules": { + "users": { + "$uid": { + ".read": "$uid === auth.uid", + ".write": "$uid === auth.uid" + } + }, + "schedule": { + "$uid": { + ".read": "$uid === auth.uid", + ".write": "$uid === auth.uid", + ".indexOn": [ + "timestamp" + ] + } + }, + "meals": { + "$uid": { + ".read": "$uid === auth.uid", + ".write": "$uid === auth.uid" + } + }, + "workouts": { + "$uid": { + ".read": "$uid === auth.uid", + ".write": "$uid === auth.uid" + } + } + } +} diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..6ae74fbe --- /dev/null +++ b/firebase.json @@ -0,0 +1,30 @@ +{ + "database": { + "rules": "database.rules.json" + }, + "hosting": { + "public": "", + "ignore": [ + "firebase.json", + ".firebaserc", + ".vscode", + ".git", + ".gitignore", + ".editorconfig", + "src/**/.*", + "database.rules.json", + "package.json", + "README.md", + "tsconfig.json", + "webpack.config.js", + "yarn.lock", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 84f8a6c9..1a04b79f 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -30,3 +30,14 @@ export const ROUTES: Routes = []; ] }) export class AppModule {} + +/* +var config = { + apiKey: "AIzaSyCXz7GrHLBs-xlsCrr185iG4v4UrNreq2Y", + authDomain: "fitness-app-e668a.firebaseapp.com", + databaseURL: "https://fitness-app-e668a.firebaseio.com", + projectId: "fitness-app-e668a", + storageBucket: "fitness-app-e668a.appspot.com", + messagingSenderId: "1014564696462" +}; +*/ \ No newline at end of file From 4fa943ffb059272ea26e48559ef2057310fb03e0 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sun, 16 Jul 2017 15:35:09 +0100 Subject: [PATCH 02/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=202:=20Auth=20m?= =?UTF-8?q?odule=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.module.ts | 4 +++- src/auth/auth.module.ts | 22 ++++++++++++++++++++++ src/auth/login/login.module.ts | 20 ++++++++++++++++++++ src/auth/register/register.module.ts | 20 ++++++++++++++++++++ src/auth/shared/shared.module.ts | 19 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/login/login.module.ts create mode 100644 src/auth/register/register.module.ts create mode 100644 src/auth/shared/shared.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1a04b79f..b66854f1 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -5,6 +5,7 @@ import { Routes, RouterModule } from '@angular/router'; import { Store } from 'store'; // feature modules +import { AuthModule } from '../auth/auth.module'; // containers import { AppComponent } from './containers/app/app.component'; @@ -17,7 +18,8 @@ export const ROUTES: Routes = []; @NgModule({ imports: [ BrowserModule, - RouterModule.forRoot(ROUTES) + RouterModule.forRoot(ROUTES), + AuthModule ], declarations: [ AppComponent diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 00000000..02525b42 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +export const ROUTES: Routes = [ + { + path: 'auth', + children: [ + { path: '', pathMatch: 'full', redirectTo: 'login' }, + { path: 'login', loadChildren: './login/login.module#LoginModule' }, + { path: 'register', loadChildren: './register/register.module#RegisterModule' }, + ] + } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule.forChild(ROUTES) + ] +}) +export class AuthModule {} \ No newline at end of file diff --git a/src/auth/login/login.module.ts b/src/auth/login/login.module.ts new file mode 100644 index 00000000..4d65f734 --- /dev/null +++ b/src/auth/login/login.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { LoginComponent } from './containers/login/login.component'; + +export const ROUTES: Routes = [ + { path: '', component: LoginComponent } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule + ], + declarations: [ + LoginComponent + ] +}) +export class LoginModule {} \ No newline at end of file diff --git a/src/auth/register/register.module.ts b/src/auth/register/register.module.ts new file mode 100644 index 00000000..1cfadc17 --- /dev/null +++ b/src/auth/register/register.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; + +import { RegisterComponent } from './containers/register/register.component'; + +export const ROUTES: Routes = [ + { path: '', component: RegisterComponent } +]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule + ], + declarations: [ + RegisterComponent + ] +}) +export class RegisterModule {} \ No newline at end of file diff --git a/src/auth/shared/shared.module.ts b/src/auth/shared/shared.module.ts new file mode 100644 index 00000000..dd925140 --- /dev/null +++ b/src/auth/shared/shared.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { AuthFormComponent } from './containers/auth-form/auth-form.component'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule + ], + declarations: [ + AuthFormComponent + ], + exports: [ + AuthFormComponent + ] +}) +export class SharedModule {} \ No newline at end of file From 5c050c66d5651d4b76b9126e717cf063bf9094c5 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sun, 16 Jul 2017 16:34:52 +0100 Subject: [PATCH 03/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=203:=20Auth=20c?= =?UTF-8?q?omponents=20and=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/containers/app/app.component.ts | 3 + .../login/containers/login/login.component.ts | 24 +++++ src/auth/login/login.module.ts | 5 +- .../containers/register/register.component.ts | 24 +++++ src/auth/register/register.module.ts | 5 +- .../auth-form/auth-form.component.scss | 90 +++++++++++++++++++ .../auth-form/auth-form.component.ts | 78 ++++++++++++++++ src/auth/shared/shared.module.ts | 2 +- 8 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/auth/login/containers/login/login.component.ts create mode 100644 src/auth/register/containers/register/register.component.ts create mode 100644 src/auth/shared/components/auth-form/auth-form.component.scss create mode 100644 src/auth/shared/components/auth-form/auth-form.component.ts diff --git a/src/app/containers/app/app.component.ts b/src/app/containers/app/app.component.ts index 74a8cb58..b48af09a 100755 --- a/src/app/containers/app/app.component.ts +++ b/src/app/containers/app/app.component.ts @@ -6,6 +6,9 @@ import { Component } from '@angular/core'; template: `
Hello Ultimate Angular! +
+ +
` }) diff --git a/src/auth/login/containers/login/login.component.ts b/src/auth/login/containers/login/login.component.ts new file mode 100644 index 00000000..1ea416de --- /dev/null +++ b/src/auth/login/containers/login/login.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'login', + template: ` +
+ +

Login

+ Not registered? + +
+
+ ` +}) +export class LoginComponent { + constructor() {} + + loginUser(event: FormGroup) { + console.log(event.value); + } +} \ No newline at end of file diff --git a/src/auth/login/login.module.ts b/src/auth/login/login.module.ts index 4d65f734..ae02bb64 100644 --- a/src/auth/login/login.module.ts +++ b/src/auth/login/login.module.ts @@ -2,6 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; + import { LoginComponent } from './containers/login/login.component'; export const ROUTES: Routes = [ @@ -11,7 +13,8 @@ export const ROUTES: Routes = [ @NgModule({ imports: [ CommonModule, - RouterModule + RouterModule.forChild(ROUTES), + SharedModule ], declarations: [ LoginComponent diff --git a/src/auth/register/containers/register/register.component.ts b/src/auth/register/containers/register/register.component.ts new file mode 100644 index 00000000..bc8a02eb --- /dev/null +++ b/src/auth/register/containers/register/register.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'register', + template: ` +
+ +

Register

+ Already have an account? + +
+
+ ` +}) +export class RegisterComponent { + constructor() {} + + registerUser(event: FormGroup) { + console.log(event.value); + } +} \ No newline at end of file diff --git a/src/auth/register/register.module.ts b/src/auth/register/register.module.ts index 1cfadc17..cb74c34c 100644 --- a/src/auth/register/register.module.ts +++ b/src/auth/register/register.module.ts @@ -2,6 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; + import { RegisterComponent } from './containers/register/register.component'; export const ROUTES: Routes = [ @@ -11,7 +13,8 @@ export const ROUTES: Routes = [ @NgModule({ imports: [ CommonModule, - RouterModule + RouterModule.forChild(ROUTES), + SharedModule ], declarations: [ RegisterComponent diff --git a/src/auth/shared/components/auth-form/auth-form.component.scss b/src/auth/shared/components/auth-form/auth-form.component.scss new file mode 100644 index 00000000..523df17e --- /dev/null +++ b/src/auth/shared/components/auth-form/auth-form.component.scss @@ -0,0 +1,90 @@ +:host ::ng-deep { + .error { + color: #a94442; + background: #f2dede; + border: 1px solid #e4b3b3; + border-radius: 2px; + padding: 8px; + font-size: 14px; + font-weight: 400; + margin: 10px 0 0; + } + h1 { + margin: 0 0 25px; + font-size: 20px; + font-weight: 600; + text-align: center; + } + button { + cursor: pointer; + outline: 0; + width: 100%; + border-radius: 2px; + border: 1px solid #1c79b8; + background: #39a1e7; + color: #fff; + padding: 10px; + font-size: 16px; + font-weight: 600; + transition: all 0.2s ease-in-out; + &:hover { + background: darken(#39a1e7, 5%); + border-color: darken(#1c79b8, 5%); + } + &:disabled { + opacity: .4; + cursor: not-allowed; + } + } + a { + display: block; + text-align: center; + color: #5e7386; + font-size: 14px; + } +} + +.auth-form { + background: #fff; + box-shadow: 0 3px 4px rgba(0,0,0,.1); + border-radius: 3px; + border: 1px solid #c1cedb; + width: 400px; + margin: 50px auto; + padding: 30px; + &__action { + margin: 10px 0 30px; + } + &__toggle { + border-radius: 0 0 3px 3px; + border-top: 1px solid #c1cedb; + background: #f8fafc; + padding: 10px; + margin: 0 -30px -30px; + } + label { + display: block; + margin: 0; + } + input { + outline: 0; + font-size: 16px; + padding: 10px 15px; + margin: 0; + width: 100%; + background: #fafcfd; + color: #5777a8; + border: 1px solid #d1deeb; + text-align: center; + &::-webkit-input-placeholder { + color: #5777a8; + } + &[type=email] { + border-radius: 3px 3px 0 0; + } + &[type=password] { + border-radius: 0 0 3px 3px; + margin: -1px 0 0; + } + } +} \ No newline at end of file diff --git a/src/auth/shared/components/auth-form/auth-form.component.ts b/src/auth/shared/components/auth-form/auth-form.component.ts new file mode 100644 index 00000000..3a9b23ef --- /dev/null +++ b/src/auth/shared/components/auth-form/auth-form.component.ts @@ -0,0 +1,78 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; + +@Component({ + selector: 'auth-form', + styleUrls: ['auth-form.component.scss'], + template: ` +
+
+ + + + + + +
+ Invalid email format +
+ +
+ Password is required +
+ + + +
+ +
+ +
+ +
+ +
+
+ ` +}) +export class AuthFormComponent { + + @Output() + submitted = new EventEmitter(); + + form = this.fb.group({ + email: ['', Validators.email], + password: ['', Validators.required] + }); + + constructor( + private fb: FormBuilder + ) {} + + onSubmit() { + if (this.form.valid) { + this.submitted.emit(this.form); + } + } + + get passwordInvalid() { + const control = this.form.get('password'); + return control.hasError('required') && control.touched; + } + + get emailFormat() { + const control = this.form.get('email'); + return control.hasError('email') && control.touched; + } + +} \ No newline at end of file diff --git a/src/auth/shared/shared.module.ts b/src/auth/shared/shared.module.ts index dd925140..d75fd9db 100644 --- a/src/auth/shared/shared.module.ts +++ b/src/auth/shared/shared.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; -import { AuthFormComponent } from './containers/auth-form/auth-form.component'; +import { AuthFormComponent } from './components/auth-form/auth-form.component'; @NgModule({ imports: [ From 6e11c131ff60cb93c512bf950ef125702650abb6 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sun, 16 Jul 2017 17:34:34 +0100 Subject: [PATCH 04/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=204:=20AngularF?= =?UTF-8?q?ire=20authentication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.module.ts | 13 +--------- src/auth/auth.module.ts | 23 +++++++++++++++++- .../login/containers/login/login.component.ts | 24 ++++++++++++++++--- .../containers/register/register.component.ts | 24 ++++++++++++++++--- src/auth/shared/services/auth/auth.service.ts | 22 +++++++++++++++++ src/auth/shared/shared.module.ts | 17 +++++++++++-- 6 files changed, 102 insertions(+), 21 deletions(-) create mode 100644 src/auth/shared/services/auth/auth.service.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b66854f1..b8961f0e 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,15 +31,4 @@ export const ROUTES: Routes = []; AppComponent ] }) -export class AppModule {} - -/* -var config = { - apiKey: "AIzaSyCXz7GrHLBs-xlsCrr185iG4v4UrNreq2Y", - authDomain: "fitness-app-e668a.firebaseapp.com", - databaseURL: "https://fitness-app-e668a.firebaseio.com", - projectId: "fitness-app-e668a", - storageBucket: "fitness-app-e668a.appspot.com", - messagingSenderId: "1014564696462" -}; -*/ \ No newline at end of file +export class AppModule {} \ No newline at end of file diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 02525b42..6ac2df66 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,6 +2,14 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; +// third-party modules +import { AngularFireModule, FirebaseAppConfig } from 'angularfire2'; +import { AngularFireAuthModule } from 'angularfire2/auth'; +import { AngularFireDatabaseModule } from 'angularfire2/database'; + +// shared modules +import { SharedModule } from './shared/shared.module'; + export const ROUTES: Routes = [ { path: 'auth', @@ -13,10 +21,23 @@ export const ROUTES: Routes = [ } ]; +export const firebaseConfig: FirebaseAppConfig = { + apiKey: "AIzaSyCXz7GrHLBs-xlsCrr185iG4v4UrNreq2Y", + authDomain: "fitness-app-e668a.firebaseapp.com", + databaseURL: "https://fitness-app-e668a.firebaseio.com", + projectId: "fitness-app-e668a", + storageBucket: "fitness-app-e668a.appspot.com", + messagingSenderId: "1014564696462" +}; + @NgModule({ imports: [ CommonModule, - RouterModule.forChild(ROUTES) + RouterModule.forChild(ROUTES), + AngularFireModule.initializeApp(firebaseConfig), + AngularFireAuthModule, + AngularFireDatabaseModule, + SharedModule.forRoot() ] }) export class AuthModule {} \ No newline at end of file diff --git a/src/auth/login/containers/login/login.component.ts b/src/auth/login/containers/login/login.component.ts index 1ea416de..31968f60 100644 --- a/src/auth/login/containers/login/login.component.ts +++ b/src/auth/login/containers/login/login.component.ts @@ -1,5 +1,8 @@ import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { AuthService } from '../../../shared/services/auth/auth.service'; @Component({ selector: 'login', @@ -11,14 +14,29 @@ import { FormGroup } from '@angular/forms'; +
+ {{ error }} +
` }) export class LoginComponent { - constructor() {} - loginUser(event: FormGroup) { - console.log(event.value); + error: string; + + constructor( + private authService: AuthService, + private router: Router + ) {} + + async loginUser(event: FormGroup) { + const { email, password } = event.value; + try { + await this.authService.loginUser(email, password); + this.router.navigate(['/']); + } catch (err) { + this.error = err.message; + } } } \ No newline at end of file diff --git a/src/auth/register/containers/register/register.component.ts b/src/auth/register/containers/register/register.component.ts index bc8a02eb..ecb8f005 100644 --- a/src/auth/register/containers/register/register.component.ts +++ b/src/auth/register/containers/register/register.component.ts @@ -1,5 +1,8 @@ import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; + +import { AuthService } from '../../../shared/services/auth/auth.service'; @Component({ selector: 'register', @@ -11,14 +14,29 @@ import { FormGroup } from '@angular/forms'; +
+ {{ error }} +
` }) export class RegisterComponent { - constructor() {} - registerUser(event: FormGroup) { - console.log(event.value); + error: string; + + constructor( + private authService: AuthService, + private router: Router + ) {} + + async registerUser(event: FormGroup) { + const { email, password } = event.value; + try { + await this.authService.createUser(email, password); + this.router.navigate(['/']); + } catch (err) { + this.error = err.message; + } } } \ No newline at end of file diff --git a/src/auth/shared/services/auth/auth.service.ts b/src/auth/shared/services/auth/auth.service.ts new file mode 100644 index 00000000..62fa9c11 --- /dev/null +++ b/src/auth/shared/services/auth/auth.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; + +import { AngularFireAuth } from 'angularfire2/auth'; + +@Injectable() +export class AuthService { + + constructor( + private af: AngularFireAuth + ) {} + + createUser(email: string, password: string) { + return this.af.auth + .createUserWithEmailAndPassword(email, password); + } + + loginUser(email: string, password: string) { + return this.af.auth + .signInWithEmailAndPassword(email, password); + } + +} \ No newline at end of file diff --git a/src/auth/shared/shared.module.ts b/src/auth/shared/shared.module.ts index d75fd9db..076c690b 100644 --- a/src/auth/shared/shared.module.ts +++ b/src/auth/shared/shared.module.ts @@ -1,9 +1,13 @@ -import { NgModule } from '@angular/core'; +import { NgModule, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; +// components import { AuthFormComponent } from './components/auth-form/auth-form.component'; +// services +import { AuthService } from './services/auth/auth.service'; + @NgModule({ imports: [ CommonModule, @@ -16,4 +20,13 @@ import { AuthFormComponent } from './components/auth-form/auth-form.component'; AuthFormComponent ] }) -export class SharedModule {} \ No newline at end of file +export class SharedModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: SharedModule, + providers: [ + AuthService + ] + }; + } +} \ No newline at end of file From 84817c6005c7643406a9f3757275efdf119ec9ac Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sun, 16 Jul 2017 18:24:35 +0100 Subject: [PATCH 05/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=205:=20Reactive?= =?UTF-8?q?=20Store=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/containers/app/app.component.ts | 32 ++++++++++++++++--- src/auth/shared/services/auth/auth.service.ts | 25 +++++++++++++++ src/store.ts | 7 +++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/app/containers/app/app.component.ts b/src/app/containers/app/app.component.ts index b48af09a..45e8d36e 100755 --- a/src/app/containers/app/app.component.ts +++ b/src/app/containers/app/app.component.ts @@ -1,17 +1,41 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; + +import { Store } from 'store'; + +import { AuthService, User } from '../../../auth/shared/services/auth/auth.service'; @Component({ selector: 'app-root', styleUrls: ['app.component.scss'], template: `
- Hello Ultimate Angular! +

{{ user$ | async | json }}

` }) -export class AppComponent { - constructor() {} +export class AppComponent implements OnInit, OnDestroy { + + user$: Observable; + subscription: Subscription; + + constructor( + private store: Store, + private authService: AuthService + ) {} + + ngOnInit() { + this.subscription = this.authService.auth$.subscribe(); + this.user$ = this.store.select('user'); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + } diff --git a/src/auth/shared/services/auth/auth.service.ts b/src/auth/shared/services/auth/auth.service.ts index 62fa9c11..60a4a7be 100644 --- a/src/auth/shared/services/auth/auth.service.ts +++ b/src/auth/shared/services/auth/auth.service.ts @@ -1,11 +1,36 @@ import { Injectable } from '@angular/core'; +import { Store } from 'store'; + +import 'rxjs/add/operator/do'; + import { AngularFireAuth } from 'angularfire2/auth'; +export interface User { + email: string, + uid: string, + authenticated: boolean +} + @Injectable() export class AuthService { + auth$ = this.af.authState + .do(next => { + if (!next) { + this.store.set('user', null); + return; + } + const user: User = { + email: next.email, + uid: next.uid, + authenticated: true + }; + this.store.set('user', user); + }); + constructor( + private store: Store, private af: AngularFireAuth ) {} diff --git a/src/store.ts b/src/store.ts index 7147daf3..ddb13a7a 100644 --- a/src/store.ts +++ b/src/store.ts @@ -4,11 +4,16 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import 'rxjs/add/operator/pluck'; import 'rxjs/add/operator/distinctUntilChanged'; +import { User } from './auth/shared/services/auth/auth.service'; + export interface State { + user: User, [key: string]: any } -const state: State = {}; +const state: State = { + user: undefined +}; export class Store { From 83b906ac4bf37faa171a5ef18ba78c153a76d4b2 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Sun, 16 Jul 2017 19:24:08 +0100 Subject: [PATCH 06/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=206:=20Logout?= =?UTF-8?q?=20and=20stateless=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.module.ts | 6 +++- .../app-header/app-header.component.scss | 33 ++++++++++++++++++ .../app-header/app-header.component.ts | 34 +++++++++++++++++++ .../components/app-nav/app-nav.component.scss | 28 +++++++++++++++ .../components/app-nav/app-nav.component.ts | 19 +++++++++++ src/app/containers/app/app.component.ts | 15 +++++++- src/auth/shared/services/auth/auth.service.ts | 4 +++ 7 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/app/components/app-header/app-header.component.scss create mode 100644 src/app/components/app-header/app-header.component.ts create mode 100644 src/app/components/app-nav/app-nav.component.scss create mode 100644 src/app/components/app-nav/app-nav.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b8961f0e..8752a8a3 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,8 @@ import { AuthModule } from '../auth/auth.module'; import { AppComponent } from './containers/app/app.component'; // components +import { AppHeaderComponent } from './components/app-header/app-header.component'; +import { AppNavComponent } from './components/app-nav/app-nav.component'; // routes export const ROUTES: Routes = []; @@ -22,7 +24,9 @@ export const ROUTES: Routes = []; AuthModule ], declarations: [ - AppComponent + AppComponent, + AppHeaderComponent, + AppNavComponent ], providers: [ Store diff --git a/src/app/components/app-header/app-header.component.scss b/src/app/components/app-header/app-header.component.scss new file mode 100644 index 00000000..568c7adb --- /dev/null +++ b/src/app/components/app-header/app-header.component.scss @@ -0,0 +1,33 @@ +.app-header { + background: #fff; + border-bottom: 1px solid #c1cedb; + padding: 15px 0; + text-align: center; + img { + display: inline-block; + } + &__user-info { + position: absolute; + top: 16px; + right: 0; + cursor: pointer; + } + span { + background: url(/img/logout.svg) no-repeat; + background-size: contain; + width: 24px; + height: 24px; + display: block; + opacity: 0.4; + &:hover { + opacity: 0.9; + } + } +} + +.wrapper { + max-width: 800px; + width: 96%; + margin: 0 auto; + position: relative; +} diff --git a/src/app/components/app-header/app-header.component.ts b/src/app/components/app-header/app-header.component.ts new file mode 100644 index 00000000..276ab13d --- /dev/null +++ b/src/app/components/app-header/app-header.component.ts @@ -0,0 +1,34 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +import { User } from '../../../auth/shared/services/auth/auth.service'; + +@Component({ + selector: 'app-header', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['app-header.component.scss'], + template: ` +
+
+ + +
+
+ ` +}) +export class AppHeaderComponent { + + @Input() + user: User; + + @Output() + logout = new EventEmitter(); + + logoutUser() { + this.logout.emit(); + } + +} \ No newline at end of file diff --git a/src/app/components/app-nav/app-nav.component.scss b/src/app/components/app-nav/app-nav.component.scss new file mode 100644 index 00000000..e5f52424 --- /dev/null +++ b/src/app/components/app-nav/app-nav.component.scss @@ -0,0 +1,28 @@ +:host { + margin: -1px 0 0; + display: block; +} +.app-nav { + background: #8022b0; + text-align: center; + a { + color: rgba(255,255,255,.6); + padding: 15px 0; + display: inline-block; + min-width: 150px; + font-weight: 500; + font-size: 16px; + text-transform: uppercase; + border-bottom: 3px solid transparent; + &:hover, + &.active { + color: #fff; + border-bottom-color: #fff; + } + } +} +.wrapper { + max-width: 800px; + width: 96%; + margin: 0 auto; +} \ No newline at end of file diff --git a/src/app/components/app-nav/app-nav.component.ts b/src/app/components/app-nav/app-nav.component.ts new file mode 100644 index 00000000..f0a494d5 --- /dev/null +++ b/src/app/components/app-nav/app-nav.component.ts @@ -0,0 +1,19 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-nav', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['app-nav.component.scss'], + template: ` +
+ +
+ ` +}) +export class AppNavComponent { + constructor() {} +} \ No newline at end of file diff --git a/src/app/containers/app/app.component.ts b/src/app/containers/app/app.component.ts index 45e8d36e..60e27796 100755 --- a/src/app/containers/app/app.component.ts +++ b/src/app/containers/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; @@ -12,7 +13,13 @@ import { AuthService, User } from '../../../auth/shared/services/auth/auth.servi styleUrls: ['app.component.scss'], template: `
-

{{ user$ | async | json }}

+ + + +
@@ -26,6 +33,7 @@ export class AppComponent implements OnInit, OnDestroy { constructor( private store: Store, + private router: Router, private authService: AuthService ) {} @@ -38,4 +46,9 @@ export class AppComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } + async onLogout() { + await this.authService.logoutUser(); + this.router.navigate(['/auth/login']); + } + } diff --git a/src/auth/shared/services/auth/auth.service.ts b/src/auth/shared/services/auth/auth.service.ts index 60a4a7be..0a491bbe 100644 --- a/src/auth/shared/services/auth/auth.service.ts +++ b/src/auth/shared/services/auth/auth.service.ts @@ -44,4 +44,8 @@ export class AuthService { .signInWithEmailAndPassword(email, password); } + logoutUser() { + return this.af.auth.signOut(); + } + } \ No newline at end of file From 88b611ac08aa70c9facfe00916e18005d18d4192 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Tue, 18 Jul 2017 18:03:19 +0100 Subject: [PATCH 07/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=207:=20Health?= =?UTF-8?q?=20Module=20and=20sub-module=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.module.ts | 4 +++- src/health/health.module.ts | 15 ++++++++++++ .../containers/meals/meals.component.scss | 0 .../meals/containers/meals/meals.component.ts | 14 +++++++++++ src/health/meals/meals.module.ts | 23 +++++++++++++++++++ .../schedule/schedule.component.scss | 0 .../containers/schedule/schedule.component.ts | 14 +++++++++++ src/health/schedule/schedule.module.ts | 23 +++++++++++++++++++ .../workouts/workouts.component.scss | 0 .../containers/workouts/workouts.component.ts | 14 +++++++++++ src/health/workouts/workouts.module.ts | 23 +++++++++++++++++++ 11 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/health/health.module.ts create mode 100644 src/health/meals/containers/meals/meals.component.scss create mode 100644 src/health/meals/containers/meals/meals.component.ts create mode 100644 src/health/meals/meals.module.ts create mode 100644 src/health/schedule/containers/schedule/schedule.component.scss create mode 100644 src/health/schedule/containers/schedule/schedule.component.ts create mode 100644 src/health/schedule/schedule.module.ts create mode 100644 src/health/workouts/containers/workouts/workouts.component.scss create mode 100644 src/health/workouts/containers/workouts/workouts.component.ts create mode 100644 src/health/workouts/workouts.module.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 8752a8a3..5831ea03 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,6 +6,7 @@ import { Store } from 'store'; // feature modules import { AuthModule } from '../auth/auth.module'; +import { HealthModule } from '../health/health.module'; // containers import { AppComponent } from './containers/app/app.component'; @@ -21,7 +22,8 @@ export const ROUTES: Routes = []; imports: [ BrowserModule, RouterModule.forRoot(ROUTES), - AuthModule + AuthModule, + HealthModule ], declarations: [ AppComponent, diff --git a/src/health/health.module.ts b/src/health/health.module.ts new file mode 100644 index 00000000..8868f4c5 --- /dev/null +++ b/src/health/health.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +export const ROUTES: Routes = [ + { path: 'schedule', loadChildren: './schedule/schedule.module#ScheduleModule' }, + { path: 'meals', loadChildren: './meals/meals.module#MealsModule' }, + { path: 'workouts', loadChildren: './workouts/workouts.module#WorkoutsModule' } +]; + +@NgModule({ + imports: [ + RouterModule.forChild(ROUTES) + ] +}) +export class HealthModule {} \ No newline at end of file diff --git a/src/health/meals/containers/meals/meals.component.scss b/src/health/meals/containers/meals/meals.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/health/meals/containers/meals/meals.component.ts b/src/health/meals/containers/meals/meals.component.ts new file mode 100644 index 00000000..f7ed3c68 --- /dev/null +++ b/src/health/meals/containers/meals/meals.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'meals', + styleUrls: ['meals.component.scss'], + template: ` +
+ Meals +
+ ` +}) +export class MealsComponent { + constructor() {} +} \ No newline at end of file diff --git a/src/health/meals/meals.module.ts b/src/health/meals/meals.module.ts new file mode 100644 index 00000000..1ee9c97c --- /dev/null +++ b/src/health/meals/meals.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +// containers +import { MealsComponent } from './containers/meals/meals.component'; + +export const ROUTES: Routes = [ + { path: '', component: MealsComponent } +]; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule.forChild(ROUTES) + ], + declarations: [ + MealsComponent + ] +}) +export class MealsModule {} \ No newline at end of file diff --git a/src/health/schedule/containers/schedule/schedule.component.scss b/src/health/schedule/containers/schedule/schedule.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/health/schedule/containers/schedule/schedule.component.ts b/src/health/schedule/containers/schedule/schedule.component.ts new file mode 100644 index 00000000..01c534a8 --- /dev/null +++ b/src/health/schedule/containers/schedule/schedule.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'schedule', + styleUrls: ['schedule.component.scss'], + template: ` +
+ Schedule +
+ ` +}) +export class ScheduleComponent { + constructor() {} +} \ No newline at end of file diff --git a/src/health/schedule/schedule.module.ts b/src/health/schedule/schedule.module.ts new file mode 100644 index 00000000..6c7e307b --- /dev/null +++ b/src/health/schedule/schedule.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +// containers +import { ScheduleComponent } from './containers/schedule/schedule.component'; + +export const ROUTES: Routes = [ + { path: '', component: ScheduleComponent } +]; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule.forChild(ROUTES) + ], + declarations: [ + ScheduleComponent + ] +}) +export class ScheduleModule {} \ No newline at end of file diff --git a/src/health/workouts/containers/workouts/workouts.component.scss b/src/health/workouts/containers/workouts/workouts.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/health/workouts/containers/workouts/workouts.component.ts b/src/health/workouts/containers/workouts/workouts.component.ts new file mode 100644 index 00000000..beb52145 --- /dev/null +++ b/src/health/workouts/containers/workouts/workouts.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'workouts', + styleUrls: ['workouts.component.scss'], + template: ` +
+ Workouts +
+ ` +}) +export class WorkoutsComponent { + constructor() {} +} \ No newline at end of file diff --git a/src/health/workouts/workouts.module.ts b/src/health/workouts/workouts.module.ts new file mode 100644 index 00000000..6ca6121a --- /dev/null +++ b/src/health/workouts/workouts.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; + +// containers +import { WorkoutsComponent } from './containers/workouts/workouts.component'; + +export const ROUTES: Routes = [ + { path: '', component: WorkoutsComponent } +]; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule.forChild(ROUTES) + ], + declarations: [ + WorkoutsComponent + ] +}) +export class WorkoutsModule {} \ No newline at end of file From 926a3b2c78ef534f84459fe7f59a47e82004bfb3 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Tue, 18 Jul 2017 18:27:30 +0100 Subject: [PATCH 08/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=208:=20Auth=20g?= =?UTF-8?q?uards=20for=20lazy=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/app.module.ts | 4 +++- src/auth/shared/guards/auth.guard.ts | 24 +++++++++++++++++++ src/auth/shared/services/auth/auth.service.ts | 4 ++++ src/auth/shared/shared.module.ts | 6 ++++- src/health/health.module.ts | 8 ++++--- 5 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/auth/shared/guards/auth.guard.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5831ea03..7f105682 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -16,7 +16,9 @@ import { AppHeaderComponent } from './components/app-header/app-header.component import { AppNavComponent } from './components/app-nav/app-nav.component'; // routes -export const ROUTES: Routes = []; +export const ROUTES: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'schedule' } +]; @NgModule({ imports: [ diff --git a/src/auth/shared/guards/auth.guard.ts b/src/auth/shared/guards/auth.guard.ts new file mode 100644 index 00000000..d1ce3b4e --- /dev/null +++ b/src/auth/shared/guards/auth.guard.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; + +import 'rxjs/add/operator/map'; + +import { AuthService } from '../services/auth/auth.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private router: Router, + private authService: AuthService + ) {} + + canActivate() { + return this.authService.authState + .map((user) => { + if (!user) { + this.router.navigate(['/auth/login']); + } + return !!user; + }); + } +} \ No newline at end of file diff --git a/src/auth/shared/services/auth/auth.service.ts b/src/auth/shared/services/auth/auth.service.ts index 0a491bbe..28f3ed7d 100644 --- a/src/auth/shared/services/auth/auth.service.ts +++ b/src/auth/shared/services/auth/auth.service.ts @@ -34,6 +34,10 @@ export class AuthService { private af: AngularFireAuth ) {} + get authState() { + return this.af.authState; + } + createUser(email: string, password: string) { return this.af.auth .createUserWithEmailAndPassword(email, password); diff --git a/src/auth/shared/shared.module.ts b/src/auth/shared/shared.module.ts index 076c690b..4f62383d 100644 --- a/src/auth/shared/shared.module.ts +++ b/src/auth/shared/shared.module.ts @@ -8,6 +8,9 @@ import { AuthFormComponent } from './components/auth-form/auth-form.component'; // services import { AuthService } from './services/auth/auth.service'; +// guards +import { AuthGuard } from './guards/auth.guard'; + @NgModule({ imports: [ CommonModule, @@ -25,7 +28,8 @@ export class SharedModule { return { ngModule: SharedModule, providers: [ - AuthService + AuthService, + AuthGuard ] }; } diff --git a/src/health/health.module.ts b/src/health/health.module.ts index 8868f4c5..ee12fa39 100644 --- a/src/health/health.module.ts +++ b/src/health/health.module.ts @@ -1,10 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '../auth/shared/guards/auth.guard'; + export const ROUTES: Routes = [ - { path: 'schedule', loadChildren: './schedule/schedule.module#ScheduleModule' }, - { path: 'meals', loadChildren: './meals/meals.module#MealsModule' }, - { path: 'workouts', loadChildren: './workouts/workouts.module#WorkoutsModule' } + { path: 'schedule', canActivate: [AuthGuard], loadChildren: './schedule/schedule.module#ScheduleModule' }, + { path: 'meals', canActivate: [AuthGuard], loadChildren: './meals/meals.module#MealsModule' }, + { path: 'workouts', canActivate: [AuthGuard], loadChildren: './workouts/workouts.module#WorkoutsModule' } ]; @NgModule({ From e32cac8be75d05e4dc91bf4318df3524a7a8a7b7 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 16:01:40 +0100 Subject: [PATCH 09/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=209:=20MealsSer?= =?UTF-8?q?vice,=20Reactive=20Store=20and=20Firebase=20Observables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/shared/services/auth/auth.service.ts | 4 +++ src/health/health.module.ts | 7 +++- .../meals/containers/meals/meals.component.ts | 32 ++++++++++++++--- src/health/meals/meals.module.ts | 5 ++- .../shared/services/meals/meals.service.ts | 35 +++++++++++++++++++ src/health/shared/shared.module.ts | 27 ++++++++++++++ src/store.ts | 5 ++- 7 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 src/health/shared/services/meals/meals.service.ts create mode 100644 src/health/shared/shared.module.ts diff --git a/src/auth/shared/services/auth/auth.service.ts b/src/auth/shared/services/auth/auth.service.ts index 28f3ed7d..d4551fa4 100644 --- a/src/auth/shared/services/auth/auth.service.ts +++ b/src/auth/shared/services/auth/auth.service.ts @@ -33,6 +33,10 @@ export class AuthService { private store: Store, private af: AngularFireAuth ) {} + + get user() { + return this.af.auth.currentUser; + } get authState() { return this.af.authState; diff --git a/src/health/health.module.ts b/src/health/health.module.ts index ee12fa39..614e5148 100644 --- a/src/health/health.module.ts +++ b/src/health/health.module.ts @@ -1,6 +1,10 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +// shared modules +import { SharedModule } from './shared/shared.module'; + +// guards import { AuthGuard } from '../auth/shared/guards/auth.guard'; export const ROUTES: Routes = [ @@ -11,7 +15,8 @@ export const ROUTES: Routes = [ @NgModule({ imports: [ - RouterModule.forChild(ROUTES) + RouterModule.forChild(ROUTES), + SharedModule.forRoot() ] }) export class HealthModule {} \ No newline at end of file diff --git a/src/health/meals/containers/meals/meals.component.ts b/src/health/meals/containers/meals/meals.component.ts index f7ed3c68..9e33fd1d 100644 --- a/src/health/meals/containers/meals/meals.component.ts +++ b/src/health/meals/containers/meals/meals.component.ts @@ -1,14 +1,38 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; + +import { Store } from 'store'; + +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; + +import { Meal, MealsService } from '../../../shared/services/meals/meals.service'; @Component({ selector: 'meals', styleUrls: ['meals.component.scss'], template: `
- Meals + {{ meals$ | async | json }}
` }) -export class MealsComponent { - constructor() {} +export class MealsComponent implements OnInit, OnDestroy { + + meals$: Observable; + subscription: Subscription; + + constructor( + private store: Store, + private mealsService: MealsService + ) {} + + ngOnInit() { + this.meals$ = this.store.select('meals'); + this.subscription = this.mealsService.meals$.subscribe(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + } \ No newline at end of file diff --git a/src/health/meals/meals.module.ts b/src/health/meals/meals.module.ts index 1ee9c97c..adefb543 100644 --- a/src/health/meals/meals.module.ts +++ b/src/health/meals/meals.module.ts @@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; + // containers import { MealsComponent } from './containers/meals/meals.component'; @@ -14,7 +16,8 @@ export const ROUTES: Routes = [ imports: [ CommonModule, ReactiveFormsModule, - RouterModule.forChild(ROUTES) + RouterModule.forChild(ROUTES), + SharedModule ], declarations: [ MealsComponent diff --git a/src/health/shared/services/meals/meals.service.ts b/src/health/shared/services/meals/meals.service.ts new file mode 100644 index 00000000..d32bbc84 --- /dev/null +++ b/src/health/shared/services/meals/meals.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { AngularFireDatabase } from 'angularfire2/database'; + +import { Store } from 'store'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; + +import { AuthService } from '../../../../auth/shared/services/auth/auth.service'; + +export interface Meal { + name: string, + ingredients: string[], + timestamp: number, + $key: string, + $exists: () => boolean +} + +@Injectable() +export class MealsService { + + meals$: Observable = this.db.list(`meals/${this.uid}`) + .do(next => this.store.set('meals', next)); + + constructor( + private store: Store, + private db: AngularFireDatabase, + private authService: AuthService + ) {} + + get uid() { + return this.authService.user.uid; + } + +} \ No newline at end of file diff --git a/src/health/shared/shared.module.ts b/src/health/shared/shared.module.ts new file mode 100644 index 00000000..72b50175 --- /dev/null +++ b/src/health/shared/shared.module.ts @@ -0,0 +1,27 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; + +// third-party modules +import { AngularFireDatabaseModule } from 'angularfire2/database'; + +import { MealsService } from './services/meals/meals.service'; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + AngularFireDatabaseModule + ], + declarations: [] +}) +export class SharedModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: SharedModule, + providers: [ + MealsService + ] + }; + } +} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts index ddb13a7a..cb6a2162 100644 --- a/src/store.ts +++ b/src/store.ts @@ -5,14 +5,17 @@ import 'rxjs/add/operator/pluck'; import 'rxjs/add/operator/distinctUntilChanged'; import { User } from './auth/shared/services/auth/auth.service'; +import { Meal } from './health/shared/services/meals/meals.service'; export interface State { user: User, + meals: Meal[], [key: string]: any } const state: State = { - user: undefined + user: undefined, + meals: undefined, }; export class Store { From 88c3030cf6cf5b211942f4f366134714f9e501a9 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 16:32:16 +0100 Subject: [PATCH 10/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=2010:=20Meal=20?= =?UTF-8?q?Container=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meals/containers/meal/meal.component.scss | 42 +++++++++++++ .../meals/containers/meal/meal.component.ts | 14 +++++ .../containers/meals/meals.component.scss | 59 +++++++++++++++++++ .../meals/containers/meals/meals.component.ts | 28 ++++++++- src/health/meals/meals.module.ts | 7 ++- 5 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 src/health/meals/containers/meal/meal.component.scss create mode 100644 src/health/meals/containers/meal/meal.component.ts diff --git a/src/health/meals/containers/meal/meal.component.scss b/src/health/meals/containers/meal/meal.component.scss new file mode 100644 index 00000000..c031c5bc --- /dev/null +++ b/src/health/meals/containers/meal/meal.component.scss @@ -0,0 +1,42 @@ +:host { + display: block; + margin: 50px 0; +} +.meal { + position: relative; + background: #fff; + box-shadow: 0 3px 4px rgba(0,0,0,.1); + border: 1px solid #c1cedb; + border-radius: 3px; + overflow: hidden; + h1 { + flex-grow: 1; + display: flex; + align-items: center; + margin: 0; + padding: 0; + font-size: 24px; + img { + margin: 0 10px 0 0; + } + } + &__title { + display: flex; + align-items: center; + padding: 30px; + background: #f6fafd; + border-bottom: 1px solid #c1cedb; + } +} +.message { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 30px; + font-size: 22px; + font-weight: 500; + img { + margin: 0 10px 0 0; + } +} \ No newline at end of file diff --git a/src/health/meals/containers/meal/meal.component.ts b/src/health/meals/containers/meal/meal.component.ts new file mode 100644 index 00000000..66981a80 --- /dev/null +++ b/src/health/meals/containers/meal/meal.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'meal', + styleUrls: ['meal.component.scss'], + template: ` +
+ I am a meal! +
+ ` +}) +export class MealComponent { + constructor() {} +} \ No newline at end of file diff --git a/src/health/meals/containers/meals/meals.component.scss b/src/health/meals/containers/meals/meals.component.scss index e69de29b..c8d4b2d1 100644 --- a/src/health/meals/containers/meals/meals.component.scss +++ b/src/health/meals/containers/meals/meals.component.scss @@ -0,0 +1,59 @@ +:host { + display: block; + margin: 50px 0; +} +.meals { + position: relative; + background: #fff; + box-shadow: 0 3px 4px rgba(0,0,0,.1); + border: 1px solid #c1cedb; + border-radius: 3px; + overflow: hidden; + h1 { + flex-grow: 1; + display: flex; + align-items: center; + margin: 0; + padding: 0; + font-size: 24px; + img { + margin: 0 10px 0 0; + } + } + &__title { + display: flex; + align-items: center; + padding: 30px; + background: #f6fafd; + border-bottom: 1px solid #c1cedb; + } +} +.btn__add { + display: flex; + align-items: center; + color: #fff; + background: #97c747; + border-radius: 50px; + padding: 6px 20px 6px 15px; + text-transform: uppercase; + font: { + weight: 600; + size: 13px; + } + img { + width: 20px; + margin: 0 6px 0 0; + } +} +.message { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 30px; + font-size: 22px; + font-weight: 500; + img { + margin: 0 10px 0 0; + } +} \ No newline at end of file diff --git a/src/health/meals/containers/meals/meals.component.ts b/src/health/meals/containers/meals/meals.component.ts index 9e33fd1d..73f4e974 100644 --- a/src/health/meals/containers/meals/meals.component.ts +++ b/src/health/meals/containers/meals/meals.component.ts @@ -11,8 +11,32 @@ import { Meal, MealsService } from '../../../shared/services/meals/meals.service selector: 'meals', styleUrls: ['meals.component.scss'], template: ` -
- {{ meals$ | async | json }} +
+
+

+ + Your meals +

+ + + New meal + +
+
+
+ + No meals, add a new meal to start +
+ +
+ +
+ + Fetching meals... +
+
` }) diff --git a/src/health/meals/meals.module.ts b/src/health/meals/meals.module.ts index adefb543..bcdb4fd9 100644 --- a/src/health/meals/meals.module.ts +++ b/src/health/meals/meals.module.ts @@ -7,9 +7,11 @@ import { SharedModule } from '../shared/shared.module'; // containers import { MealsComponent } from './containers/meals/meals.component'; +import { MealComponent } from './containers/meal/meal.component'; export const ROUTES: Routes = [ - { path: '', component: MealsComponent } + { path: '', component: MealsComponent }, + { path: 'new', component: MealComponent }, ]; @NgModule({ @@ -20,7 +22,8 @@ export const ROUTES: Routes = [ SharedModule ], declarations: [ - MealsComponent + MealsComponent, + MealComponent ] }) export class MealsModule {} \ No newline at end of file From 72d459617b0f765f5843345b4280bc82517f1209 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 17:29:43 +0100 Subject: [PATCH 11/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=2011:=20Meal=20?= =?UTF-8?q?Reactive=20Forms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meal-form/meal-form.component.scss | 174 ++++++++++++++++++ .../meal-form/meal-form.component.ts | 110 +++++++++++ .../meals/containers/meal/meal.component.ts | 20 +- src/health/meals/meals.module.ts | 6 +- 4 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/health/meals/components/meal-form/meal-form.component.scss create mode 100644 src/health/meals/components/meal-form/meal-form.component.ts diff --git a/src/health/meals/components/meal-form/meal-form.component.scss b/src/health/meals/components/meal-form/meal-form.component.scss new file mode 100644 index 00000000..0c259ca5 --- /dev/null +++ b/src/health/meals/components/meal-form/meal-form.component.scss @@ -0,0 +1,174 @@ +%button { + outline: 0; + cursor: pointer; + border: 0; + background: transparent; +} +.confirm, +.cancel { + @extend %button; + padding: 5px 10px; + margin: 0 0 0 5px; + font-size: 14px; +} +.error { + color: #a94442; + background: #f2dede; + border: 1px solid #e4b3b3; + border-radius: 2px; + padding: 8px; + font-size: 14px; + font-weight: 400; + margin: 10px 0 0; +} +.confirm { + color: #fff; + background: #d73a49; + border-radius: 3px; + transition: all .2s ease-in-out; + &:hover { + background: darken(#d73a49, 3%); + } +} + +.meal-form { + &__name { + padding: 30px; + flex-direction: column; + border-bottom: 1px solid #d1deeb; + } + &__food { + padding: 30px; + border-bottom: 1px solid #d1deeb; + } + &__subtitle { + display: flex; + align-items: center; + h3 { + margin: 20px 0; + flex-grow: 1; + } + } + &__delete { + display: flex; + align-items: center; + .cancel { + margin: 0 20px 0 0; + } + } + &__add { + display: flex; + align-items: center; + color: #fff; + border: 0; + outline: 0; + cursor: pointer; + background: #97c747; + border-radius: 50px; + padding: 6px 20px 6px 15px; + text-transform: uppercase; + font-weight: 600; + font-size: 13px; + img { + width: 20px; + margin: 0 6px 0 0; + } + } + &__remove { + cursor: pointer; + background-image: url(/img/cross.svg); + background-size: 15px 15px; + background-repeat: no-repeat; + background-position: center center; + background-color: #eff4f9; + width: 35px; + height: 38px; + display: block; + position: absolute; + top: 1px; + right: 1px; + border-left: 1px solid #d1deeb; + transition: all .2s ease-in-out; + &:hover { + background-color: darken(#eff4f9, 5%); + } + } + &__submit { + display: flex; + justify-content: space-between; + padding: 30px; + } + h1 { + flex-grow: 1; + display: flex; + align-items: center; + margin: 0; + padding: 0; + font-size: 24px; + img { + margin: 0 10px 0 0; + } + } + h3 { + font-size: 18px; + font-weight: 600; + } + label { + position: relative; + display: block; + margin: 0 0 10px; + } + input { + outline: 0; + font-size: 16px; + padding: 10px 40px 10px 15px; + margin: 0; + width: 100%; + background: #fff; + color: #545e6f; + flex-grow: 1; + border: 1px solid #d1deeb; + border-radius: 3px; + transition: all 0.2s ease-in-out; + &:focus { + border-color: #a5b9ce; + } + &::-webkit-input-placeholder { + color: #aaa; + } + } + .button { + cursor: pointer; + outline: 0; + border: 0; + border-radius: 2px; + background: #39a1e7; + color: #fff; + padding: 10px 18px; + font-size: 16px; + font-weight: 600; + transition: all 0.2s ease-in-out; + display: inline-block; + &:hover { + background: darken(#39a1e7, 5%); + } + &:disabled { + opacity: .4; + cursor: not-allowed; + } + &--cancel { + background: #fff; + color: #545e6f; + &:hover { + background: #fff; + } + } + &--delete { + background: #d73a49; + align-self: flex-start; + &:hover { + background: darken(#d73a49, 5%); + } + } + } +} diff --git a/src/health/meals/components/meal-form/meal-form.component.ts b/src/health/meals/components/meal-form/meal-form.component.ts new file mode 100644 index 00000000..59b6196c --- /dev/null +++ b/src/health/meals/components/meal-form/meal-form.component.ts @@ -0,0 +1,110 @@ +import { Component, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormArray, FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; + +import { Meal } from '../../../shared/services/meals/meals.service'; + +@Component({ + selector: 'meal-form', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['meal-form.component.scss'], + template: ` +
+ +
+ +
+ +
+ +
+
+

Food

+ +
+
+ +
+
+ +
+
+ + + Cancel + +
+
+ +
+ +
+ ` +}) +export class MealFormComponent { + + @Output() + create = new EventEmitter(); + + form = this.fb.group({ + name: ['', Validators.required], + ingredients: this.fb.array(['']) + }); + + constructor( + private fb: FormBuilder + ) {} + + get required() { + return ( + this.form.get('name').hasError('required') && + this.form.get('name').touched + ); + } + + get ingredients() { + return this.form.get('ingredients') as FormArray; + } + + addIngredient() { + this.ingredients.push(new FormControl('')); + } + + removeIngredient(index: number) { + this.ingredients.removeAt(index); + } + + createMeal() { + if (this.form.valid) { + this.create.emit(this.form.value); + } + } + +} \ No newline at end of file diff --git a/src/health/meals/containers/meal/meal.component.ts b/src/health/meals/containers/meal/meal.component.ts index 66981a80..f29bd071 100644 --- a/src/health/meals/containers/meal/meal.component.ts +++ b/src/health/meals/containers/meal/meal.component.ts @@ -1,14 +1,32 @@ import { Component } from '@angular/core'; +import { Meal } from '../../../shared/services/meals/meals.service'; + @Component({ selector: 'meal', styleUrls: ['meal.component.scss'], template: `
- I am a meal! +
+

+ + Create meal +

+
+
+ + +
` }) export class MealComponent { + constructor() {} + + addMeal(event: Meal) { + console.log('Meal:', event); + } + } \ No newline at end of file diff --git a/src/health/meals/meals.module.ts b/src/health/meals/meals.module.ts index bcdb4fd9..6694846e 100644 --- a/src/health/meals/meals.module.ts +++ b/src/health/meals/meals.module.ts @@ -5,6 +5,9 @@ import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; +// components +import { MealFormComponent } from './components/meal-form/meal-form.component'; + // containers import { MealsComponent } from './containers/meals/meals.component'; import { MealComponent } from './containers/meal/meal.component'; @@ -23,7 +26,8 @@ export const ROUTES: Routes = [ ], declarations: [ MealsComponent, - MealComponent + MealComponent, + MealFormComponent ] }) export class MealsModule {} \ No newline at end of file From d9f1b3b6d9f731d4eeafc3c021a10554c1bdf7b1 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 18:23:54 +0100 Subject: [PATCH 12/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=2012:=20Statele?= =?UTF-8?q?ss=20ListItem,=20add/remove=20Meal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meals/containers/meal/meal.component.ts | 17 +++-- .../meals/containers/meals/meals.component.ts | 10 ++- src/health/meals/meals.module.ts | 1 + .../list-item/list-item.component.scss | 72 +++++++++++++++++++ .../list-item/list-item.component.ts | 71 ++++++++++++++++++ .../shared/services/meals/meals.service.ts | 8 +++ src/health/shared/shared.module.ts | 11 ++- 7 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 src/health/shared/components/list-item/list-item.component.scss create mode 100644 src/health/shared/components/list-item/list-item.component.ts diff --git a/src/health/meals/containers/meal/meal.component.ts b/src/health/meals/containers/meal/meal.component.ts index f29bd071..986a05f9 100644 --- a/src/health/meals/containers/meal/meal.component.ts +++ b/src/health/meals/containers/meal/meal.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; +import { Router } from '@angular/router'; -import { Meal } from '../../../shared/services/meals/meals.service'; +import { MealsService, Meal } from '../../../shared/services/meals/meals.service'; @Component({ selector: 'meal', @@ -23,10 +24,18 @@ import { Meal } from '../../../shared/services/meals/meals.service'; }) export class MealComponent { - constructor() {} + constructor( + private mealsService: MealsService, + private router: Router + ) {} - addMeal(event: Meal) { - console.log('Meal:', event); + async addMeal(event: Meal) { + await this.mealsService.addMeal(event); + this.backToMeals(); + } + + backToMeals() { + this.router.navigate(['meals']); } } \ No newline at end of file diff --git a/src/health/meals/containers/meals/meals.component.ts b/src/health/meals/containers/meals/meals.component.ts index 73f4e974..5cf7adec 100644 --- a/src/health/meals/containers/meals/meals.component.ts +++ b/src/health/meals/containers/meals/meals.component.ts @@ -29,7 +29,11 @@ import { Meal, MealsService } from '../../../shared/services/meals/meals.service No meals, add a new meal to start
- + +
@@ -59,4 +63,8 @@ export class MealsComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } + removeMeal(event: Meal) { + this.mealsService.removeMeal(event.$key); + } + } \ No newline at end of file diff --git a/src/health/meals/meals.module.ts b/src/health/meals/meals.module.ts index 6694846e..91f469fc 100644 --- a/src/health/meals/meals.module.ts +++ b/src/health/meals/meals.module.ts @@ -15,6 +15,7 @@ import { MealComponent } from './containers/meal/meal.component'; export const ROUTES: Routes = [ { path: '', component: MealsComponent }, { path: 'new', component: MealComponent }, + { path: ':id', component: MealComponent }, ]; @NgModule({ diff --git a/src/health/shared/components/list-item/list-item.component.scss b/src/health/shared/components/list-item/list-item.component.scss new file mode 100644 index 00000000..e060d834 --- /dev/null +++ b/src/health/shared/components/list-item/list-item.component.scss @@ -0,0 +1,72 @@ +.list-item { + display: flex; + border-bottom: 1px solid #c1cedb; + transition: all .2s ease-in-out; + &:hover { + background-color: #f9f9f9; + } + p { + margin: 0; + } + &__name { + flex-grow: 1; + } + &__ingredients { + font-size: 12px; + color: #8ea6bd; + font-style: italic; + } + &__delete { + display: flex; + align-items: center; + margin: 0 10px 0 0; + p { + margin: 0 10px 0 0; + font-size: 14px; + } + } + a { + display: flex; + flex-grow: 1; + flex-direction: column; + height: 100%; + padding: 12px 20px; + font-weight: 400; + color: #545e6f; + font-size: 16px; + } +} +%button { + outline: 0; + cursor: pointer; + border: 0; +} +.confirm, +.cancel { + @extend %button; + padding: 5px 10px; + margin: 0 0 0 5px; + font-size: 14px; +} +.confirm { + color: #fff; + background: #d73a49; + border-radius: 3px; + transition: all .2s ease-in-out; + &:hover { + background: darken(#d73a49, 3%); + } +} +.cancel { + background: transparent; +} +.trash { + @extend %button; + border-left: 1px solid #c1cedb; + padding: 10px 15px; + background: #f6fafd; + transition: all .2s ease-in-out; + &:hover { + background-color: darken(#f6fafd, 2%); + } +} \ No newline at end of file diff --git a/src/health/shared/components/list-item/list-item.component.ts b/src/health/shared/components/list-item/list-item.component.ts new file mode 100644 index 00000000..cad622ae --- /dev/null +++ b/src/health/shared/components/list-item/list-item.component.ts @@ -0,0 +1,71 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'list-item', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['list-item.component.scss'], + template: ` +
+ + +

{{ item.name }}

+

+ + {{ item.ingredients }} + +

+ +
+ +
+

Delete item?

+ + +
+ + + +
+ ` +}) +export class ListItemComponent { + + toggled = false; + + @Input() + item: any; + + @Output() + remove = new EventEmitter(); + + constructor() {} + + toggle() { + this.toggled = !this.toggled; + } + + removeItem() { + this.remove.emit(this.item); + } + + getRoute(item: any) { + return [`../meals`, item.$key]; + } +} \ No newline at end of file diff --git a/src/health/shared/services/meals/meals.service.ts b/src/health/shared/services/meals/meals.service.ts index d32bbc84..034eb1b5 100644 --- a/src/health/shared/services/meals/meals.service.ts +++ b/src/health/shared/services/meals/meals.service.ts @@ -32,4 +32,12 @@ export class MealsService { return this.authService.user.uid; } + addMeal(meal: Meal) { + return this.db.list(`meals/${this.uid}`).push(meal); + } + + removeMeal(key: string) { + return this.db.list(`meals/${this.uid}`).remove(key); + } + } \ No newline at end of file diff --git a/src/health/shared/shared.module.ts b/src/health/shared/shared.module.ts index 72b50175..315342b6 100644 --- a/src/health/shared/shared.module.ts +++ b/src/health/shared/shared.module.ts @@ -5,6 +5,10 @@ import { RouterModule } from '@angular/router'; // third-party modules import { AngularFireDatabaseModule } from 'angularfire2/database'; +// components +import { ListItemComponent } from './components/list-item/list-item.component'; + +// services import { MealsService } from './services/meals/meals.service'; @NgModule({ @@ -13,7 +17,12 @@ import { MealsService } from './services/meals/meals.service'; RouterModule, AngularFireDatabaseModule ], - declarations: [] + declarations: [ + ListItemComponent + ], + exports: [ + ListItemComponent + ] }) export class SharedModule { static forRoot(): ModuleWithProviders { From 62df260a66ba9a3e11232b07df66d137e1a7b968 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 19:17:03 +0100 Subject: [PATCH 13/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=2013:=20Route?= =?UTF-8?q?=20params=20and=20switchMap=20to=20Store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meals/containers/meal/meal.component.ts | 33 ++++++++++++++++--- .../shared/services/meals/meals.service.ts | 10 ++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/health/meals/containers/meal/meal.component.ts b/src/health/meals/containers/meal/meal.component.ts index 986a05f9..e40902ae 100644 --- a/src/health/meals/containers/meal/meal.component.ts +++ b/src/health/meals/containers/meal/meal.component.ts @@ -1,5 +1,9 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; + +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/switchMap'; import { MealsService, Meal } from '../../../shared/services/meals/meals.service'; @@ -11,7 +15,12 @@ import { MealsService, Meal } from '../../../shared/services/meals/meals.service

- Create meal + + {{ meal.name ? 'Edit' : 'Create' }} meal + + + Loading... +

@@ -22,13 +31,27 @@ import { MealsService, Meal } from '../../../shared/services/meals/meals.service
` }) -export class MealComponent { +export class MealComponent implements OnInit, OnDestroy { + + meal$: Observable; + subscription: Subscription; constructor( private mealsService: MealsService, - private router: Router + private router: Router, + private route: ActivatedRoute ) {} + ngOnInit() { + this.subscription = this.mealsService.meals$.subscribe(); + this.meal$ = this.route.params + .switchMap(param => this.mealsService.getMeal(param.id)); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + async addMeal(event: Meal) { await this.mealsService.addMeal(event); this.backToMeals(); diff --git a/src/health/shared/services/meals/meals.service.ts b/src/health/shared/services/meals/meals.service.ts index 034eb1b5..3a86f03f 100644 --- a/src/health/shared/services/meals/meals.service.ts +++ b/src/health/shared/services/meals/meals.service.ts @@ -5,6 +5,9 @@ import { Store } from 'store'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/observable/of'; import { AuthService } from '../../../../auth/shared/services/auth/auth.service'; @@ -32,6 +35,13 @@ export class MealsService { return this.authService.user.uid; } + getMeal(key: string) { + if (!key) return Observable.of({}); + return this.store.select('meals') + .filter(Boolean) + .map(meals => meals.find((meal: Meal) => meal.$key === key)); + } + addMeal(meal: Meal) { return this.db.list(`meals/${this.uid}`).push(meal); } From ad80f38891bc8101fefe90eaa02d19a689ff4d85 Mon Sep 17 00:00:00 2001 From: Todd Motto Date: Thu, 20 Jul 2017 20:15:42 +0100 Subject: [PATCH 14/14] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Step=2014:=20Reactiv?= =?UTF-8?q?e=20Form=20editing,=20CRUD=20methods=20to=20Firebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meal-form/meal-form.component.scss | 7 ++ .../meal-form/meal-form.component.ts | 84 ++++++++++++++++++- .../meals/containers/meal/meal.component.ts | 25 +++++- .../shared/services/meals/meals.service.ts | 4 + 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/health/meals/components/meal-form/meal-form.component.scss b/src/health/meals/components/meal-form/meal-form.component.scss index 0c259ca5..e40a0a94 100644 --- a/src/health/meals/components/meal-form/meal-form.component.scss +++ b/src/health/meals/components/meal-form/meal-form.component.scss @@ -52,6 +52,13 @@ &__delete { display: flex; align-items: center; + > div { + display: flex; + align-items: center; + p { + margin: 0; + } + } .cancel { margin: 0 20px 0 0; } diff --git a/src/health/meals/components/meal-form/meal-form.component.ts b/src/health/meals/components/meal-form/meal-form.component.ts index 59b6196c..f6eb7025 100644 --- a/src/health/meals/components/meal-form/meal-form.component.ts +++ b/src/health/meals/components/meal-form/meal-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnChanges, SimpleChanges, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; import { FormArray, FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; import { Meal } from '../../../shared/services/meals/meals.service'; @@ -52,15 +52,46 @@ import { Meal } from '../../../shared/services/meals/meals.service'; + Cancel
+ +
+
+

Delete item?

+ + +
+ + +
+ @@ -68,11 +99,23 @@ import { Meal } from '../../../shared/services/meals/meals.service'; ` }) -export class MealFormComponent { +export class MealFormComponent implements OnChanges { + + toggled = false; + exists = false; + + @Input() + meal: Meal; @Output() create = new EventEmitter(); + @Output() + update = new EventEmitter(); + + @Output() + remove = new EventEmitter(); + form = this.fb.group({ name: ['', Validators.required], ingredients: this.fb.array(['']) @@ -82,6 +125,29 @@ export class MealFormComponent { private fb: FormBuilder ) {} + ngOnChanges(changes: SimpleChanges) { + if (this.meal && this.meal.name) { + this.exists = true; + this.emptyIngredients(); + + const value = this.meal; + this.form.patchValue(value); + + if (value.ingredients) { + for (const item of value.ingredients) { + this.ingredients.push(new FormControl(item)); + } + } + + } + } + + emptyIngredients() { + while(this.ingredients.controls.length) { + this.ingredients.removeAt(0); + } + } + get required() { return ( this.form.get('name').hasError('required') && @@ -107,4 +173,18 @@ export class MealFormComponent { } } + updateMeal() { + if (this.form.valid) { + this.update.emit(this.form.value); + } + } + + removeMeal() { + this.remove.emit(this.form.value); + } + + toggle() { + this.toggled = !this.toggled; + } + } \ No newline at end of file diff --git a/src/health/meals/containers/meal/meal.component.ts b/src/health/meals/containers/meal/meal.component.ts index e40902ae..15356676 100644 --- a/src/health/meals/containers/meal/meal.component.ts +++ b/src/health/meals/containers/meal/meal.component.ts @@ -23,11 +23,20 @@ import { MealsService, Meal } from '../../../shared/services/meals/meals.service
-
+
+ [meal]="meal" + (create)="addMeal($event)" + (update)="updateMeal($event)" + (remove)="removeMeal($event)">
+ +
+ + Fetching meal... +
+
` }) @@ -57,6 +66,18 @@ export class MealComponent implements OnInit, OnDestroy { this.backToMeals(); } + async updateMeal(event: Meal) { + const key = this.route.snapshot.params.id; + await this.mealsService.updateMeal(key, event); + this.backToMeals(); + } + + async removeMeal(event: Meal) { + const key = this.route.snapshot.params.id; + await this.mealsService.removeMeal(key); + this.backToMeals(); + } + backToMeals() { this.router.navigate(['meals']); } diff --git a/src/health/shared/services/meals/meals.service.ts b/src/health/shared/services/meals/meals.service.ts index 3a86f03f..f9209fdf 100644 --- a/src/health/shared/services/meals/meals.service.ts +++ b/src/health/shared/services/meals/meals.service.ts @@ -46,6 +46,10 @@ export class MealsService { return this.db.list(`meals/${this.uid}`).push(meal); } + updateMeal(key: string, meal: Meal) { + return this.db.object(`meals/${this.uid}/${key}`).update(meal); + } + removeMeal(key: string) { return this.db.list(`meals/${this.uid}`).remove(key); }