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..7f105682 100755
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -5,22 +5,32 @@ import { Routes, RouterModule } from '@angular/router';
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';
// components
+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: [
BrowserModule,
- RouterModule.forRoot(ROUTES)
+ RouterModule.forRoot(ROUTES),
+ AuthModule,
+ HealthModule
],
declarations: [
- AppComponent
+ AppComponent,
+ AppHeaderComponent,
+ AppNavComponent
],
providers: [
Store
@@ -29,4 +39,4 @@ export const ROUTES: Routes = [];
AppComponent
]
})
-export class AppModule {}
+export class AppModule {}
\ No newline at end of file
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 74a8cb58..60e27796 100755
--- a/src/app/containers/app/app.component.ts
+++ b/src/app/containers/app/app.component.ts
@@ -1,14 +1,54 @@
-import { Component } from '@angular/core';
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Router } from '@angular/router';
+
+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!
+
+
+
+
+
+
+
`
})
-export class AppComponent {
- constructor() {}
+export class AppComponent implements OnInit, OnDestroy {
+
+ user$: Observable;
+ subscription: Subscription;
+
+ constructor(
+ private store: Store,
+ private router: Router,
+ private authService: AuthService
+ ) {}
+
+ ngOnInit() {
+ this.subscription = this.authService.auth$.subscribe();
+ this.user$ = this.store.select('user');
+ }
+
+ ngOnDestroy() {
+ this.subscription.unsubscribe();
+ }
+
+ async onLogout() {
+ await this.authService.logoutUser();
+ this.router.navigate(['/auth/login']);
+ }
+
}
diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts
new file mode 100644
index 00000000..6ac2df66
--- /dev/null
+++ b/src/auth/auth.module.ts
@@ -0,0 +1,43 @@
+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',
+ children: [
+ { path: '', pathMatch: 'full', redirectTo: 'login' },
+ { path: 'login', loadChildren: './login/login.module#LoginModule' },
+ { path: 'register', loadChildren: './register/register.module#RegisterModule' },
+ ]
+ }
+];
+
+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),
+ 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
new file mode 100644
index 00000000..31968f60
--- /dev/null
+++ b/src/auth/login/containers/login/login.component.ts
@@ -0,0 +1,42 @@
+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',
+ template: `
+
+ `
+})
+export class LoginComponent {
+
+ 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/login/login.module.ts b/src/auth/login/login.module.ts
new file mode 100644
index 00000000..ae02bb64
--- /dev/null
+++ b/src/auth/login/login.module.ts
@@ -0,0 +1,23 @@
+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 = [
+ { path: '', component: LoginComponent }
+];
+
+@NgModule({
+ imports: [
+ CommonModule,
+ RouterModule.forChild(ROUTES),
+ SharedModule
+ ],
+ declarations: [
+ LoginComponent
+ ]
+})
+export class LoginModule {}
\ 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
new file mode 100644
index 00000000..ecb8f005
--- /dev/null
+++ b/src/auth/register/containers/register/register.component.ts
@@ -0,0 +1,42 @@
+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',
+ template: `
+
+ `
+})
+export class RegisterComponent {
+
+ 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/register/register.module.ts b/src/auth/register/register.module.ts
new file mode 100644
index 00000000..cb74c34c
--- /dev/null
+++ b/src/auth/register/register.module.ts
@@ -0,0 +1,23 @@
+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 = [
+ { path: '', component: RegisterComponent }
+];
+
+@NgModule({
+ imports: [
+ CommonModule,
+ RouterModule.forChild(ROUTES),
+ SharedModule
+ ],
+ declarations: [
+ RegisterComponent
+ ]
+})
+export class RegisterModule {}
\ No newline at end of file
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: `
+
+ `
+})
+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/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
new file mode 100644
index 00000000..d4551fa4
--- /dev/null
+++ b/src/auth/shared/services/auth/auth.service.ts
@@ -0,0 +1,59 @@
+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
+ ) {}
+
+ get user() {
+ return this.af.auth.currentUser;
+ }
+
+ get authState() {
+ return this.af.authState;
+ }
+
+ createUser(email: string, password: string) {
+ return this.af.auth
+ .createUserWithEmailAndPassword(email, password);
+ }
+
+ loginUser(email: string, password: string) {
+ return this.af.auth
+ .signInWithEmailAndPassword(email, password);
+ }
+
+ logoutUser() {
+ return this.af.auth.signOut();
+ }
+
+}
\ 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..4f62383d
--- /dev/null
+++ b/src/auth/shared/shared.module.ts
@@ -0,0 +1,36 @@
+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';
+
+// guards
+import { AuthGuard } from './guards/auth.guard';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule
+ ],
+ declarations: [
+ AuthFormComponent
+ ],
+ exports: [
+ AuthFormComponent
+ ]
+})
+export class SharedModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: SharedModule,
+ providers: [
+ AuthService,
+ AuthGuard
+ ]
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/health/health.module.ts b/src/health/health.module.ts
new file mode 100644
index 00000000..614e5148
--- /dev/null
+++ b/src/health/health.module.ts
@@ -0,0 +1,22 @@
+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 = [
+ { 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({
+ imports: [
+ RouterModule.forChild(ROUTES),
+ SharedModule.forRoot()
+ ]
+})
+export class HealthModule {}
\ No newline at end of file
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..e40a0a94
--- /dev/null
+++ b/src/health/meals/components/meal-form/meal-form.component.scss
@@ -0,0 +1,181 @@
+%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;
+ > div {
+ display: flex;
+ align-items: center;
+ p {
+ margin: 0;
+ }
+ }
+ .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..f6eb7025
--- /dev/null
+++ b/src/health/meals/components/meal-form/meal-form.component.ts
@@ -0,0 +1,190 @@
+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';
+
+@Component({
+ selector: 'meal-form',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrls: ['meal-form.component.scss'],
+ template: `
+
+ `
+})
+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([''])
+ });
+
+ constructor(
+ 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') &&
+ 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);
+ }
+ }
+
+ 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.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..15356676
--- /dev/null
+++ b/src/health/meals/containers/meal/meal.component.ts
@@ -0,0 +1,85 @@
+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';
+
+@Component({
+ selector: 'meal',
+ styleUrls: ['meal.component.scss'],
+ template: `
+
+
+
+
+
+ {{ meal.name ? 'Edit' : 'Create' }} meal
+
+
+ Loading...
+
+
+
+
+
+
+
+
+
+

+ Fetching meal...
+
+
+
+ `
+})
+export class MealComponent implements OnInit, OnDestroy {
+
+ meal$: Observable;
+ subscription: Subscription;
+
+ constructor(
+ private mealsService: MealsService,
+ 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();
+ }
+
+ 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']);
+ }
+
+}
\ 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..c8d4b2d1
--- /dev/null
+++ 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
new file mode 100644
index 00000000..5cf7adec
--- /dev/null
+++ b/src/health/meals/containers/meals/meals.component.ts
@@ -0,0 +1,70 @@
+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: `
+
+
+
+
+

+ No meals, add a new meal to start
+
+
+
+
+
+
+

+ Fetching meals...
+
+
+
+ `
+})
+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();
+ }
+
+ 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
new file mode 100644
index 00000000..91f469fc
--- /dev/null
+++ b/src/health/meals/meals.module.ts
@@ -0,0 +1,34 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+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';
+
+export const ROUTES: Routes = [
+ { path: '', component: MealsComponent },
+ { path: 'new', component: MealComponent },
+ { path: ':id', component: MealComponent },
+];
+
+@NgModule({
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ RouterModule.forChild(ROUTES),
+ SharedModule
+ ],
+ declarations: [
+ MealsComponent,
+ MealComponent,
+ MealFormComponent
+ ]
+})
+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/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: `
+
+ `
+})
+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
new file mode 100644
index 00000000..f9209fdf
--- /dev/null
+++ b/src/health/shared/services/meals/meals.service.ts
@@ -0,0 +1,57 @@
+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 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/map';
+import 'rxjs/add/observable/of';
+
+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;
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+}
\ 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..315342b6
--- /dev/null
+++ b/src/health/shared/shared.module.ts
@@ -0,0 +1,36 @@
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { CommonModule } from '@angular/common';
+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({
+ imports: [
+ CommonModule,
+ RouterModule,
+ AngularFireDatabaseModule
+ ],
+ declarations: [
+ ListItemComponent
+ ],
+ exports: [
+ ListItemComponent
+ ]
+})
+export class SharedModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: SharedModule,
+ providers: [
+ MealsService
+ ]
+ };
+ }
+}
\ 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
diff --git a/src/store.ts b/src/store.ts
index 7147daf3..cb6a2162 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -4,11 +4,19 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject';
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 = {};
+const state: State = {
+ user: undefined,
+ meals: undefined,
+};
export class Store {