├── trainer ├── .gitignore ├── static │ ├── audio │ │ ├── 321.wav │ │ ├── tick.mp3 │ │ ├── lunge.wav │ │ ├── nextup.mp3 │ │ ├── nextup.wav │ │ ├── plank.wav │ │ ├── squats.wav │ │ ├── stepup.wav │ │ ├── 15seconds.wav │ │ ├── crunches.wav │ │ ├── highknees.wav │ │ ├── pushups.wav │ │ ├── sideplank.wav │ │ ├── tick10s.mp3 │ │ ├── wallsit.wav │ │ ├── tricepdips.wav │ │ ├── jumpingjacks.wav │ │ └── pushupandrotate.wav │ ├── images │ │ ├── Plank.png │ │ ├── rest.png │ │ ├── squat.png │ │ ├── start.png │ │ ├── Pushup.png │ │ ├── crunches.png │ │ ├── finish.png │ │ ├── lunges.png │ │ ├── running.png │ │ ├── wallsit.png │ │ ├── highknees.png │ │ ├── sideplank.png │ │ ├── tricepdips.png │ │ ├── JumpingJacks.png │ │ ├── pushupNRotate.png │ │ └── stepUpOntoChair.png │ └── css │ │ └── app.css ├── src │ ├── bootstrap.ts │ ├── components │ │ ├── workout-builder │ │ │ ├── navigation │ │ │ │ ├── sub-nav.component.ts │ │ │ │ ├── left-nav-main.component.ts │ │ │ │ ├── left-nav-main.component.html │ │ │ │ ├── left-nav-exercises.component.html │ │ │ │ ├── sub-nav.component.html │ │ │ │ └── left-nav-exercises.component.ts │ │ │ ├── alphanumeric-validator.ts │ │ │ ├── workout-builder.component.ts │ │ │ ├── exercises │ │ │ │ ├── exercises.component.html │ │ │ │ └── exercises.component.ts │ │ │ ├── shared │ │ │ │ ├── busy-indicator.directive.ts │ │ │ │ ├── remote-validator.directive.ts │ │ │ │ ├── ajax-button.component.ts │ │ │ │ └── remote-validator.directive.spec.ts │ │ │ ├── workouts │ │ │ │ ├── workouts.component.html │ │ │ │ └── workouts.component.ts │ │ │ ├── workout │ │ │ │ ├── workout.guard.ts │ │ │ │ ├── workout.component.ts │ │ │ │ └── workout.component.html │ │ │ ├── exercise │ │ │ │ ├── exercise.guard.ts │ │ │ │ ├── exercise.component.html │ │ │ │ └── exercise.component.ts │ │ │ ├── workout-builder.routes.ts │ │ │ ├── builder-services │ │ │ │ ├── exercise-builder-service.ts │ │ │ │ └── workout-builder-service.ts │ │ │ └── workout-builder.module.ts │ │ ├── workout-runner │ │ │ ├── exercise-description │ │ │ │ ├── exercise-description.component.ts │ │ │ │ └── exercise-description.html │ │ │ ├── workout-audio │ │ │ │ ├── workout-audio.html │ │ │ │ ├── my-audio.directive.ts │ │ │ │ ├── workout-audio.component.ts │ │ │ │ └── workout-audio0.component.ts │ │ │ ├── workout-container │ │ │ │ ├── workout-container.html │ │ │ │ └── workout-container.component.ts │ │ │ ├── video-player │ │ │ │ ├── video-player.html │ │ │ │ ├── video-player.component.ts │ │ │ │ └── video-dialog.component.ts │ │ │ ├── workout-runner.module.ts │ │ │ ├── workout-runner.html │ │ │ ├── workout-runner.component.ts │ │ │ └── workout-runner.component.spec.ts │ │ ├── finish │ │ │ ├── finish.module.ts │ │ │ ├── finish.html │ │ │ └── finish.component.ts │ │ ├── shared │ │ │ ├── search.pipe.ts │ │ │ ├── seconds-to-time.pipe.spec.ts │ │ │ ├── shared.module.ts │ │ │ ├── seconds-to-time.pipe.ts │ │ │ └── order-by.pipe.ts │ │ ├── workout-history │ │ │ ├── workout-history.module.ts │ │ │ ├── workout-history.component.ts │ │ │ └── workout-history.html │ │ ├── start │ │ │ ├── start.module.ts │ │ │ ├── start.component.ts │ │ │ └── start.html │ │ └── app │ │ │ ├── header.component.ts │ │ │ ├── app.component.ts │ │ │ ├── app.routes.ts │ │ │ └── app.module.ts │ ├── services │ │ ├── local-storage.ts │ │ ├── services.module.ts │ │ ├── model.ts │ │ ├── workout-history-tracker.ts │ │ ├── workout-service.spec.ts │ │ └── workout-service.ts │ └── tsconfig.json ├── tests │ ├── protractor.conf.js │ ├── Readme.md │ ├── e2e │ │ └── workout-runner.e2e.ts │ ├── karma-test-shim.js │ └── karma.conf.js ├── README.md ├── index.html ├── gulpfile.js ├── package.json ├── systemjs.config.js └── db │ └── seed.js ├── guessthenumber ├── app │ ├── main.ts │ ├── app.module.ts │ └── guess-the-number.component.ts ├── readme.md ├── index.html └── systemjs.config.js ├── README.md └── LICENSE /trainer/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | typings/** -------------------------------------------------------------------------------- /trainer/static/audio/321.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/321.wav -------------------------------------------------------------------------------- /trainer/static/audio/tick.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/tick.mp3 -------------------------------------------------------------------------------- /trainer/static/audio/lunge.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/lunge.wav -------------------------------------------------------------------------------- /trainer/static/audio/nextup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/nextup.mp3 -------------------------------------------------------------------------------- /trainer/static/audio/nextup.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/nextup.wav -------------------------------------------------------------------------------- /trainer/static/audio/plank.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/plank.wav -------------------------------------------------------------------------------- /trainer/static/audio/squats.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/squats.wav -------------------------------------------------------------------------------- /trainer/static/audio/stepup.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/stepup.wav -------------------------------------------------------------------------------- /trainer/static/images/Plank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/Plank.png -------------------------------------------------------------------------------- /trainer/static/images/rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/rest.png -------------------------------------------------------------------------------- /trainer/static/images/squat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/squat.png -------------------------------------------------------------------------------- /trainer/static/images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/start.png -------------------------------------------------------------------------------- /trainer/static/audio/15seconds.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/15seconds.wav -------------------------------------------------------------------------------- /trainer/static/audio/crunches.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/crunches.wav -------------------------------------------------------------------------------- /trainer/static/audio/highknees.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/highknees.wav -------------------------------------------------------------------------------- /trainer/static/audio/pushups.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/pushups.wav -------------------------------------------------------------------------------- /trainer/static/audio/sideplank.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/sideplank.wav -------------------------------------------------------------------------------- /trainer/static/audio/tick10s.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/tick10s.mp3 -------------------------------------------------------------------------------- /trainer/static/audio/wallsit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/wallsit.wav -------------------------------------------------------------------------------- /trainer/static/images/Pushup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/Pushup.png -------------------------------------------------------------------------------- /trainer/static/images/crunches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/crunches.png -------------------------------------------------------------------------------- /trainer/static/images/finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/finish.png -------------------------------------------------------------------------------- /trainer/static/images/lunges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/lunges.png -------------------------------------------------------------------------------- /trainer/static/images/running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/running.png -------------------------------------------------------------------------------- /trainer/static/images/wallsit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/wallsit.png -------------------------------------------------------------------------------- /trainer/static/audio/tricepdips.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/tricepdips.wav -------------------------------------------------------------------------------- /trainer/static/images/highknees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/highknees.png -------------------------------------------------------------------------------- /trainer/static/images/sideplank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/sideplank.png -------------------------------------------------------------------------------- /trainer/static/images/tricepdips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/tricepdips.png -------------------------------------------------------------------------------- /trainer/static/audio/jumpingjacks.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/jumpingjacks.wav -------------------------------------------------------------------------------- /trainer/static/images/JumpingJacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/JumpingJacks.png -------------------------------------------------------------------------------- /trainer/static/images/pushupNRotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/pushupNRotate.png -------------------------------------------------------------------------------- /trainer/static/audio/pushupandrotate.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/audio/pushupandrotate.wav -------------------------------------------------------------------------------- /trainer/static/images/stepUpOntoChair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandermani/angular2byexample/HEAD/trainer/static/images/stepUpOntoChair.png -------------------------------------------------------------------------------- /trainer/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './components/app/app.module'; 3 | platformBrowserDynamic().bootstrapModule(AppModule); 4 | -------------------------------------------------------------------------------- /trainer/tests/protractor.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | specs: ['e2e/*.e2e.js'], 3 | baseUrl: 'http://localhost:9000/index.html', 4 | seleniumAddress: 'http://localhost:4444/wd/hub', 5 | useAllAngular2AppRoots: true 6 | } 7 | -------------------------------------------------------------------------------- /guessthenumber/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | const platform = platformBrowserDynamic(); 6 | platform.bootstrapModule(AppModule); 7 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/sub-nav.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'sub-nav', 5 | templateUrl: '/src/components/workout-builder/navigation/sub-nav.component.html' 6 | }) 7 | export class SubNavComponent{ 8 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/left-nav-main.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'left-nav-main', 5 | templateUrl: '/src/components/workout-builder/navigation/left-nav-main.component.html' 6 | }) 7 | export class LeftNavMainComponent{ 8 | } -------------------------------------------------------------------------------- /trainer/src/services/local-storage.ts: -------------------------------------------------------------------------------- 1 | export class LocalStorage { 2 | getItem(key: string): T { 3 | if (localStorage[key]) { 4 | return JSON.parse(localStorage[key]); 5 | } 6 | return null; 7 | } 8 | 9 | setItem(key: string, item: any) { 10 | localStorage[key] = JSON.stringify(item); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/left-nav-main.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Workouts 4 | Exercises 5 |
6 |
-------------------------------------------------------------------------------- /guessthenumber/readme.md: -------------------------------------------------------------------------------- 1 | Guess the number game written with `Angular2` and `TypeScript`. 2 | 3 | Requires a static file server to execute 4 | 5 | You can use [live-server][1], by installing 6 | 7 | ```bash 8 | npm install -g live-server 9 | ``` 10 | Once installed just do 11 | 12 | ```bash 13 | live-server 14 | ``` 15 | Start guessing! 16 | 17 | [1]:https://www.npmjs.com/package/live-server 18 | -------------------------------------------------------------------------------- /guessthenumber/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { GuessTheNumberComponent } from './guess-the-number.component'; 5 | 6 | @NgModule({ 7 | imports: [ BrowserModule ], 8 | declarations: [ GuessTheNumberComponent ], 9 | bootstrap: [ GuessTheNumberComponent ] 10 | }) 11 | 12 | export class AppModule { } 13 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/alphanumeric-validator.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from '@angular/forms'; 2 | 3 | export class AlphaNumericValidator { 4 | static invalidAlphaNumeric(control: FormControl):{ [key:string]:boolean } { 5 | if ( control.value.length && !control.value.match(/^[a-z0-9]+$/i) ){ 6 | return {invalidAlphaNumeric: true }; 7 | } 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/exercise-description/exercise-description.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'exercise-description', 5 | templateUrl: '/src/components/workout-runner/exercise-description/exercise-description.html', 6 | }) 7 | export class ExerciseDescriptionComponent { 8 | @Input() description: string; 9 | @Input() steps: string; 10 | } 11 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-audio/workout-audio.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /trainer/src/components/finish/finish.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | 5 | import { FinishComponent } from './finish.component'; 6 | 7 | @NgModule({ 8 | imports: [ 9 | BrowserModule, 10 | RouterModule], 11 | declarations: [FinishComponent], 12 | exports: [FinishComponent], 13 | }) 14 | export class FinishModule { } -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/left-nav-exercises.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Exercises

3 |
4 |
5 | 6 |
7 |
8 |
-------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout-builder.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 7 |
8 | 9 |
` 10 | }) 11 | export class WorkoutBuilderComponent{ 12 | } -------------------------------------------------------------------------------- /trainer/src/components/shared/search.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'search', 5 | pure: false 6 | }) 7 | export class SearchPipe { 8 | transform(value: Array, field:string, searchTerm:string): any { 9 | if (!field) return []; 10 | if (searchTerm == null || searchTerm.length === 0) return [...value]; 11 | let results = value.filter((item) => item[field] === searchTerm); 12 | return results.length === 0 ? null : results; 13 | } 14 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-container/workout-container.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /trainer/src/components/workout-history/workout-history.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { WorkoutHistoryComponent } from './workout-history.component'; 4 | import {SharedModule} from '../shared/shared.module'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | BrowserModule, 9 | SharedModule], 10 | declarations: [WorkoutHistoryComponent], 11 | exports: [WorkoutHistoryComponent], 12 | }) 13 | export class WorkoutHistoryModule { } -------------------------------------------------------------------------------- /trainer/src/components/shared/seconds-to-time.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import {SecondsToTimePipe} from "./seconds-to-time.pipe"; 2 | 3 | describe('SecondsToTime pipe', () => { 4 | let pipe:SecondsToTimePipe; 5 | beforeEach(() => { 6 | pipe = new SecondsToTimePipe(); 7 | }); 8 | 9 | it('should convert integer to time format', () => { 10 | expect(pipe.transform(5)).toEqual('00:00:05'); 11 | expect(pipe.transform(65)).toEqual('00:01:05'); 12 | expect(pipe.transform(3610)).toEqual('01:00:10'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/sub-nav.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Home 4 | 5 | 6 | New Workout 7 | 8 | 9 | New Exercise 10 | 11 |
-------------------------------------------------------------------------------- /trainer/src/services/services.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { LocalStorage } from './local-storage'; 5 | import { WorkoutHistoryTracker } from './workout-history-tracker'; 6 | import { WorkoutService } from './workout-service'; 7 | 8 | @NgModule({ 9 | imports: [], 10 | declarations: [], 11 | providers: [ 12 | LocalStorage, 13 | WorkoutHistoryTracker, 14 | WorkoutService], 15 | }) 16 | export class ServicesModule { } -------------------------------------------------------------------------------- /trainer/src/components/finish/finish.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Well Done!

5 |
6 | 7 |
8 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/exercises/exercises.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

Exercises

6 |
7 |
{{exercise.title}}
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /trainer/src/components/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import {OrderByPipe} from './order-by.pipe'; 5 | import {SearchPipe} from './search.pipe'; 6 | import {SecondsToTimePipe} from "./seconds-to-time.pipe"; 7 | 8 | @NgModule({ 9 | imports: [], 10 | declarations: [OrderByPipe, 11 | SecondsToTimePipe, 12 | SearchPipe], 13 | exports: [ 14 | OrderByPipe, 15 | SecondsToTimePipe, 16 | SearchPipe], 17 | }) 18 | export class SharedModule { } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/exercise-description/exercise-description.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Description

5 |
6 |
7 | {{description}} 8 |
9 |
10 |
11 |
12 |

Steps

13 |
14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /trainer/src/components/start/start.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { RouterModule } from '@angular/router'; 4 | import { SharedModule } from "../shared/shared.module"; 5 | import { FormsModule } from '@angular/forms'; 6 | 7 | import { StartComponent } from './start.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | FormsModule, 13 | RouterModule, 14 | SharedModule], 15 | declarations: [StartComponent], 16 | exports: [StartComponent], 17 | }) 18 | export class StartModule { } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/video-player/video-player.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Videos

4 |
5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/shared/busy-indicator.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, HostBinding} from '@angular/core'; 2 | import {NgModel} from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: `[a2beBusyIndicator][ngModel]`, 6 | }) 7 | export class BusyIndicatorDirective { 8 | private get _validating(): boolean { return this.model.control != null && this.model.control.pending; }; 9 | @HostBinding("style.borderWidth") get controlBorderWidth(): string { return this._validating ? "3px" : null; }; 10 | @HostBinding("style.borderColor") get controlBorderColor(): string { return this._validating ? "gray" : null }; 11 | 12 | constructor(private model: NgModel) { } 13 | } 14 | -------------------------------------------------------------------------------- /trainer/src/components/shared/seconds-to-time.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'secondsToTime' 5 | }) 6 | export class SecondsToTimePipe implements PipeTransform { 7 | transform(value: number): any { 8 | if (!isNaN(value)) { 9 | var hours = Math.floor(value / 3600); 10 | var minutes = Math.floor((value - (hours * 3600)) / 60); 11 | var seconds = value - (hours * 3600) - (minutes * 60); 12 | 13 | return ("0" + hours).substr(-2) + ':' 14 | + ("0" + minutes).substr(-2) + ':' 15 | + ("0" + seconds).substr(-2); 16 | } 17 | return; 18 | } 19 | } -------------------------------------------------------------------------------- /trainer/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "isolatedModules": false, 7 | "jsx": "react", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "declaration": false, 11 | "noImplicitAny": false, 12 | "removeComments": true, 13 | "noLib": false, 14 | "preserveConstEnums": true, 15 | "suppressImplicitAnyIndexErrors": true 16 | }, 17 | "filesGlob": [ 18 | "**/*.ts", 19 | "**/*.tsx", 20 | "!node_modules/**" 21 | ], 22 | "files": [ 23 | "bootstrap.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /trainer/src/components/finish/finish.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute,Router } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'finish', 6 | templateUrl: '/src/components/finish/finish.html', 7 | }) 8 | export class FinishComponent implements OnInit, OnDestroy { 9 | private workoutName: string; 10 | private sub: any; 11 | 12 | constructor(private route:ActivatedRoute, 13 | private router:Router) { 14 | } 15 | 16 | ngOnInit() { 17 | this.sub = this.route.params.subscribe(params => { 18 | this.workoutName = params['id']; 19 | }) 20 | } 21 | 22 | ngOnDestroy() { 23 | this.sub.unsubscribe(); 24 | } 25 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-history/workout-history.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {WorkoutHistoryTracker, WorkoutLogEntry} from '../../services/workout-history-tracker'; 3 | import {Location} from '@angular/common'; 4 | 5 | @Component({ 6 | selector: 'workout-history', 7 | templateUrl: `/src/components/workout-history/workout-history.html` 8 | }) 9 | export class WorkoutHistoryComponent { 10 | history: Array = []; 11 | completed: boolean; 12 | constructor(private tracker: WorkoutHistoryTracker, private location: Location) { } 13 | 14 | ngOnInit() { 15 | this.history = this.tracker.getHistory(); 16 | } 17 | 18 | goBack() { 19 | this.location.back(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-audio/my-audio.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef} from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'audio', 5 | exportAs: 'MyAudio' 6 | }) 7 | export class MyAudioDirective { 8 | private audioPlayer: HTMLAudioElement; 9 | constructor(element: ElementRef) { 10 | this.audioPlayer = element.nativeElement; 11 | } 12 | 13 | stop() { 14 | this.audioPlayer.pause(); 15 | } 16 | start() { 17 | this.audioPlayer.play(); 18 | } 19 | get currentTime(): number { 20 | return this.audioPlayer.currentTime; 21 | } 22 | get duration(): number { 23 | return this.audioPlayer.duration; 24 | } 25 | get playbackComplete() { 26 | return this.duration == this.currentTime; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /trainer/tests/Readme.md: -------------------------------------------------------------------------------- 1 | To run the unit test follow these steps (Run them from command line, from `tests` parent folder): 2 | 3 | 1. Install karma `npm install karma --save-dev`. (The karma version against which the code was tested is `0.13.22`) 4 | 2. Install karma CLI `npm install -g karma-cli`. (The karma-cli version was `1.0.1`) 5 | 3. Run karma wizard `karma init` and use the default options. This should install the necessary dependencies and create a new config file in the parent folder. *DELETE IT*. The config file in this folder needs to be used. 6 | 4. Install Jasmine `npm install jasmine-core --save-dev --save-exact`. (The jasmine version against which the code was tested is `2.4.1`) 7 | 5. To run the test first start the application `gulp play` then do `karma start tests/karma.conf.js`. 8 | -------------------------------------------------------------------------------- /trainer/src/components/app/header.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Router, Event } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'header', 6 | template: ` 9 | ` 12 | }) 13 | export class HeaderComponent { 14 | showHistoryLink: boolean = true; 15 | private subscription: any; 16 | constructor(private router: Router) { 17 | this.router.events.subscribe((data: Event) => { 18 | this.showHistoryLink = !this.router.url.startsWith('/workout'); 19 | }); 20 | } 21 | } -------------------------------------------------------------------------------- /guessthenumber/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Guess the Number! 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Loading... 18 | 19 | 20 | -------------------------------------------------------------------------------- /trainer/src/components/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewContainerRef} from '@angular/core'; 2 | import { Overlay } from 'angular2-modal'; 3 | 4 | import {WorkoutHistoryComponent} from '../workout-history/workout-history.component'; 5 | 6 | @Component({ 7 | selector: 'trainer-app', 8 | template: ` 13 |
14 | 15 |
` 16 | }) 17 | export class TrainerAppComponent { 18 | constructor(overlay: Overlay, viewContainer: ViewContainerRef) { 19 | overlay.defaultViewContainer = viewContainer; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /trainer/src/components/shared/order-by.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe} from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'orderBy' 5 | }) 6 | export class OrderByPipe { 7 | transform(value: Array, field:string): any { 8 | if (value == null || value.length == 1) { 9 | return value; 10 | } 11 | if (field.startsWith("-")) { 12 | field = field.substring(1); 13 | if (typeof value[0][field] === 'string' || value[0][field] instanceof String) { 14 | return [...value].sort((a, b) => b[field].localeCompare(a[field])); 15 | } 16 | return [...value].sort((a, b) => b[field] - a[field]); 17 | } 18 | else { 19 | if (typeof value[0][field] === 'string' || value[0][field] instanceof String) { 20 | return [...value].sort((a, b) => -b[field].localeCompare(a[field])); 21 | } 22 | return [...value].sort((a, b) => a[field] - b[field]); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/exercises/exercises.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable } from 'rxjs/Rx'; 4 | 5 | import { Exercise } from "../../../services/model"; 6 | import { WorkoutService } from "../../../services/workout-service"; 7 | 8 | @Component({ 9 | selector: 'exercises', 10 | templateUrl: '/src/components/workout-builder/exercises/exercises.component.html', 11 | }) 12 | export class ExercisesComponent implements OnInit{ 13 | exerciseList:Observable; 14 | errorMessage: any; 15 | 16 | constructor( 17 | public router:Router, 18 | public workoutService:WorkoutService) {} 19 | 20 | ngOnInit() { 21 | this.exerciseList = this.workoutService.getExercises(); 22 | } 23 | 24 | onSelect(exercise:Exercise) { 25 | this.router.navigate(['./builder/exercise', exercise.name]); 26 | } 27 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular2 By Example 2 | 3 | [![Angular2 By Example Front Cover](https://d1ldz4te4covpm.cloudfront.net/sites/default/files/imagecache/ppv4_main_book_cover/7192OS_5079_Angular%202%20By%20Example.jpg)](https://www.packtpub.com/web-development/angular-2-example) 4 | 5 | Source code repository for the book [Angular2 by Example](https://www.packtpub.com/web-development/angular-2-example) 6 | 7 | To setup code for Guess The Number see the README.md in **guessthenumber** folder. 8 | 9 | To setup code for Personal Trainer see the README.md in **trainer** folder. 10 | 11 | ## Note 12 | 13 | The **master** branch contains the final outcome for both the samples we build throughout the book. 14 | 15 | Chapter progress is tracked using individual branches. Each **chapter has checkpoints** and each checkpoint code is **available on a seperate branch**. 16 | 17 | For example, branches *base*, *checkpoint2.1*, *checkpoint2.2*, *checkpoint2.3* and *checkpoint2.4* contain code checkpoints for **Chapter 2**. 18 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/shared/remote-validator.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input} from '@angular/core'; 2 | import { NG_ASYNC_VALIDATORS, Validators, Validator, FormControl } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: `[a2beRemoteValidator][ngModel]`, 6 | providers:[{ provide: NG_ASYNC_VALIDATORS, useExisting: RemoteValidatorDirective, multi: true }] 7 | }) 8 | 9 | export class RemoteValidatorDirective implements Validator { 10 | @Input("a2beRemoteValidator") validationKey: string; 11 | @Input("validateFunction") execute: (value: string) => Promise; 12 | 13 | validate(control: FormControl): { [key: string]: any } { 14 | let value: string = control.value; 15 | return this.execute(value).then((result: boolean) => { 16 | if (result) { 17 | return null; 18 | } 19 | else { 20 | let error: any = {}; 21 | error[this.validationKey] = true; 22 | return error; 23 | } 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-container/workout-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy} from '@angular/core'; 2 | import { ActivatedRoute, Router} from '@angular/router'; 3 | 4 | import { WorkoutAudioComponent } from '../workout-audio/workout-audio.component'; 5 | import { WorkoutRunnerComponent } from '../workout-runner.component'; 6 | 7 | @Component({ 8 | selector: 'workout-container', 9 | templateUrl: '/src/components/workout-runner/workout-container/workout-container.html' 10 | }) 11 | export class WorkoutContainerCompnent implements OnInit, OnDestroy { 12 | private workoutName: string; 13 | private sub: any; 14 | 15 | constructor(private route:ActivatedRoute, 16 | private router:Router) { 17 | } 18 | 19 | ngOnInit() { 20 | this.sub = this.route.params.subscribe(params => { 21 | this.workoutName = params['id']; 22 | }) 23 | } 24 | 25 | ngOnDestroy() { 26 | this.sub.unsubscribe(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/shared/ajax-button.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, HostListener} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ajax-button', 5 | template: `` 11 | }) 12 | export class AjaxButtonComponent { 13 | busy: boolean = null; 14 | @Input() execute: any; 15 | @Input() parameter: any; 16 | 17 | @HostListener('click', ['$event']) 18 | onClick(event: any) { 19 | let result: any = this.execute(this.parameter); 20 | if (result instanceof Promise) { 21 | this.busy = true; 22 | result.then( 23 | () => { 24 | this.busy = null; 25 | }, (error: any) => { 26 | this.busy = null; 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/navigation/left-nav-exercises.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs/Rx'; 3 | 4 | import { Exercise, ExercisePlan} from "../../../services/model"; 5 | import { WorkoutBuilderService } from "../builder-services/workout-builder-service"; 6 | import { WorkoutService } from "../../../services/workout-service"; 7 | 8 | @Component({ 9 | selector: 'left-nav-exercises', 10 | templateUrl: '/src/components/workout-builder/navigation/left-nav-exercises.component.html' 11 | }) 12 | export class LeftNavExercisesComponent implements OnInit{ 13 | exerciseList:Observable; 14 | 15 | constructor( 16 | public workoutService:WorkoutService, 17 | public workoutBuilderService:WorkoutBuilderService) {} 18 | 19 | ngOnInit() { 20 | this.exerciseList = this.workoutService.getExercises(); 21 | } 22 | 23 | addExercise(exercise:Exercise) { 24 | this.workoutBuilderService.addExercise(new ExercisePlan(exercise, 30)); 25 | } 26 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workouts/workouts.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

Workouts

6 |
Could not load the specific workout!
7 |
8 |
{{workout.title}}
9 |
10 | - {{ workout.totalWorkoutDuration()|secondsToTime}} 11 | - {{workout.exercises.length}} 12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /trainer/src/components/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { WorkoutContainerCompnent } from '../workout-runner/workout-container/workout-container.component'; 5 | import { StartComponent } from '../start/start.component'; 6 | import { FinishComponent } from '../finish/finish.component'; 7 | import { WorkoutHistoryComponent } from '../workout-history/workout-history.component'; 8 | 9 | const workoutBuilderRoutes: Routes = [ 10 | { 11 | path: 'builder', 12 | loadChildren: 'dist/components/workout-builder/workout-builder.module#WorkoutBuilderModule' 13 | } 14 | ]; 15 | 16 | export const routes: Routes = [ 17 | { path: 'start', component: StartComponent }, 18 | { path: 'workout/:id', component: WorkoutContainerCompnent }, 19 | { path: 'finish', component: FinishComponent }, 20 | { path: 'history', component: WorkoutHistoryComponent }, 21 | ...workoutBuilderRoutes, 22 | { path: '**', redirectTo: '/start' } 23 | ]; 24 | export const routing: ModuleWithProviders = RouterModule.forRoot(routes); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/video-player/video-player.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | import {SafeResourceUrl} from '@angular/platform-browser'; 3 | import { Modal } from 'angular2-modal/plugins/bootstrap'; 4 | import { overlayConfigFactory } from 'angular2-modal' 5 | import {VideoDialogComponent, VideoDialogContext} from './video-dialog.component'; 6 | 7 | @Component({ 8 | selector: 'video-player', 9 | templateUrl: '/src/components/workout-runner/video-player/video-player.html' 10 | }) 11 | export class VideoPlayerComponent { 12 | @Input() videos: Array; 13 | @Output() playbackStarted: EventEmitter = new EventEmitter(); 14 | @Output() playbackEnded: EventEmitter = new EventEmitter(); 15 | 16 | constructor(private modal: Modal) { } 17 | 18 | playVideo(videoId: string) { 19 | this.playbackStarted.emit(null); 20 | 21 | var dialog = this.modal.open(VideoDialogComponent, overlayConfigFactory(new VideoDialogContext(videoId))); 22 | dialog 23 | .then((d) => d.result) 24 | .then(() => { this.playbackEnded.emit(null); }, (error) => { this.playbackEnded.emit(null); }); 25 | }; 26 | } -------------------------------------------------------------------------------- /trainer/src/components/start/start.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | 4 | import { WorkoutPlan } from "../../services/model"; 5 | import {WorkoutService } from "../../services/workout-service"; 6 | 7 | @Component({ 8 | selector: 'start', 9 | templateUrl: '/src/components/start/start.html', 10 | }) 11 | export class StartComponent implements OnInit, OnDestroy{ 12 | public workoutList:Array = []; 13 | public notFound:boolean = false; 14 | public searchTerm: string; 15 | private subscription:any; 16 | 17 | constructor(private router:Router, 18 | private workoutService:WorkoutService) { 19 | } 20 | 21 | ngOnInit() { 22 | this.subscription = this.workoutService.getWorkouts() 23 | .subscribe( 24 | workoutList => this.workoutList = workoutList, 25 | (err:any) => console.error(err) 26 | ); 27 | } 28 | 29 | onSelect(workout:WorkoutPlan) { 30 | this.router.navigate(['/workout', workout.name]); 31 | } 32 | 33 | ngOnDestroy() { 34 | this.subscription.unsubscribe(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /trainer/src/components/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { HttpModule } from '@angular/http'; 4 | 5 | import { TrainerAppComponent } from './app.component'; 6 | import { HeaderComponent } from './header.component'; 7 | 8 | import { WorkoutRunnerModule } from '../workout-runner/workout-runner.module'; 9 | import { StartModule } from '../start/start.module'; 10 | import { FinishModule } from '../finish/finish.module'; 11 | import { ServicesModule } from '../../services/services.module'; 12 | import { WorkoutHistoryModule } from '../workout-history/workout-history.module'; 13 | 14 | import { ModalModule } from 'angular2-modal'; 15 | import { BootstrapModalModule } from 'angular2-modal/plugins/bootstrap'; 16 | 17 | import {routing} from './app.routes'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | BrowserModule, 22 | HttpModule, 23 | WorkoutRunnerModule, 24 | StartModule, 25 | FinishModule, 26 | routing, 27 | ModalModule.forRoot(), 28 | BootstrapModalModule, 29 | ServicesModule, 30 | WorkoutHistoryModule], 31 | declarations: [ 32 | TrainerAppComponent, 33 | HeaderComponent], 34 | bootstrap: [TrainerAppComponent] 35 | }) 36 | export class AppModule { } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-runner.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SharedModule } from "../shared/shared.module"; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | 5 | import { WorkoutRunnerComponent } from './workout-runner.component'; 6 | import { WorkoutContainerCompnent } from './workout-container/workout-container.component'; 7 | 8 | import {ExerciseDescriptionComponent} from './exercise-description/exercise-description.component'; 9 | import {VideoPlayerComponent} from './video-player/video-player.component'; 10 | import {VideoDialogComponent} from './video-player/video-dialog.component'; 11 | import {WorkoutAudioComponent} from './workout-audio/workout-audio.component'; 12 | import {MyAudioDirective} from './workout-audio/my-audio.directive'; 13 | 14 | @NgModule({ 15 | imports: [BrowserModule, 16 | SharedModule], 17 | declarations: [ 18 | WorkoutRunnerComponent, 19 | ExerciseDescriptionComponent, 20 | VideoPlayerComponent, 21 | VideoDialogComponent, 22 | WorkoutContainerCompnent, 23 | WorkoutAudioComponent, 24 | MyAudioDirective], 25 | exports: [WorkoutContainerCompnent], 26 | entryComponents:[VideoDialogComponent] 27 | }) 28 | export class WorkoutRunnerModule { } -------------------------------------------------------------------------------- /trainer/README.md: -------------------------------------------------------------------------------- 1 | # Personal Trainer 2 | 3 | Personal Trainer built using Angular2 and TypeScript 4 | 5 | ## Install 6 | 7 | Clone this repo and execute in your favourite shell: 8 | 9 | * `npm i -g gulp` to install gulp globally (if you don't have it installed already) 10 | * `npm install` to install local npm dependencies 11 | 12 | ## Play 13 | 14 | After completing installation type in your favourite shell: 15 | 16 | * `gulp play` to start the app in a new browser window. App files are observed and will be re-transpiled on each change. 17 | 18 | > ~~If you see a bunch of **TypeScript** compilation errors while running `gulp play`, the required **typings** did not get installed. While `npm install` should also install the typings, at times this does not happen. 19 | > If the typing installation throws error try to upgrade the typing global installation with command `npm install typings -g` and then run the command `npm run typings install` again.~~ 20 | 21 | > The old approach of using the `typings` tool to install typings has been abandoned in favour of **npm** based typing supported by new versions of **TypeScript** compiler. Take latest of the code and upgrade to latest version of TypeScript compiler. `npm install` should now install all typings. 22 | 23 | > **Note**: The book content still show use of `typings`, you can disregard it. 24 | -------------------------------------------------------------------------------- /guessthenumber/systemjs.config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | map : { 3 | 'app': 'app', 4 | 'rxjs': 'https://unpkg.com/rxjs@5.0.0-beta.12', 5 | '@angular/common': 'https://unpkg.com/@angular/common@2.0.0', 6 | '@angular/compiler': 'https://unpkg.com/@angular/compiler@2.0.0', 7 | '@angular/core': 'https://unpkg.com/@angular/core@2.0.0', 8 | '@angular/platform-browser': 'https://unpkg.com/@angular/platform-browser@2.0.0', 9 | '@angular/platform-browser-dynamic': 'https://unpkg.com/@angular/platform-browser-dynamic@2.0.0' 10 | }, 11 | packages:{ 12 | 'app': { main: 'main.ts', defaultExtension: 'ts' }, 13 | '@angular/common': { main: 'bundles/common.umd.js', defaultExtension: 'js' }, 14 | '@angular/compiler': { main: 'bundles/compiler.umd.js', defaultExtension: 'js' }, 15 | '@angular/core': { main: 'bundles/core.umd.js', defaultExtension: 'js' }, 16 | '@angular/platform-browser': { main: 'bundles/platform-browser.umd.js', defaultExtension: 'js' }, 17 | '@angular/platform-browser-dynamic': { main: 'bundles/platform-browser-dynamic.umd.js', defaultExtension: 'js' }, 18 | }, 19 | // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER 20 | transpiler: 'typescript', 21 | typescriptOptions: { 22 | emitDecoratorMetadata: true 23 | } 24 | }); -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout/workout.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivate, Router} from '@angular/router'; 3 | import {Observable} from "rxjs/Rx"; 4 | 5 | import { WorkoutPlan } from "../../../services/model"; 6 | import { WorkoutService } from "../../../services/workout-service"; 7 | 8 | @Injectable() 9 | export class WorkoutGuard implements CanActivate { 10 | workout: WorkoutPlan; 11 | 12 | constructor( 13 | public workoutService:WorkoutService, 14 | public router:Router) { 15 | } 16 | 17 | canActivate(route:ActivatedRouteSnapshot):Observable { 18 | let workoutName = route.params['id']; 19 | return this.workoutService.getWorkout(workoutName) 20 | .take(1) 21 | .map(workout => !!workout) 22 | .do(workoutExists => { 23 | if (!workoutExists) this.router.navigate(['/builder/workouts/workout-not-found']); 24 | }) 25 | .catch(error => { 26 | if (error.status === 404) { 27 | this.router.navigate(['/builder/workouts/workout-not-found']); 28 | return Observable.of(false) 29 | } else { 30 | return Observable.throw(error); 31 | } 32 | } 33 | ) 34 | } 35 | } -------------------------------------------------------------------------------- /trainer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Personal Trainer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Loading... 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workouts/workouts.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, OnDestroy } from '@angular/core'; 2 | import {ActivatedRoute, Router } from '@angular/router'; 3 | 4 | import { WorkoutPlan } from "../../../services/model"; 5 | import { WorkoutService } from "../../../services/workout-service"; 6 | 7 | @Component({ 8 | selector: 'workouts', 9 | templateUrl: '/src/components/workout-builder/workouts/workouts.component.html' 10 | }) 11 | export class WorkoutsComponent implements OnInit, OnDestroy { 12 | public workoutList:Array = []; 13 | public notFound:boolean = false; 14 | private subscription:any; 15 | 16 | constructor( 17 | public route:ActivatedRoute, 18 | public router:Router, 19 | public workoutService:WorkoutService) { 20 | } 21 | 22 | ngOnInit() { 23 | if(this.route.snapshot.url[1] && this.route.snapshot.url[1].path === 'workout-not-found') this.notFound = true; 24 | this.subscription = this.workoutService.getWorkouts() 25 | .subscribe( 26 | (workoutList: WorkoutPlan[]) => this.workoutList = workoutList, 27 | (err: any) => console.error(err) 28 | ); 29 | } 30 | 31 | onSelect(workout:WorkoutPlan) { 32 | this.router.navigate(['./builder/workout', workout.name]); 33 | } 34 | 35 | ngOnDestroy() { 36 | this.subscription.unsubscribe(); 37 | } 38 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/exercise/exercise.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from "rxjs/Rx"; 4 | 5 | import { Exercise } from "../../../services/model"; 6 | import { WorkoutService } from "../../../services/workout-service"; 7 | 8 | @Injectable() 9 | export class ExerciseGuard implements CanActivate { 10 | exercise: Exercise; 11 | sub: any; 12 | 13 | constructor( 14 | public workoutService: WorkoutService, 15 | public router: Router) {} 16 | 17 | canActivate(route:ActivatedRouteSnapshot, 18 | state:RouterStateSnapshot):Observable { 19 | let exerciseName = route.params['id']; 20 | return this.workoutService.getExercise(exerciseName) 21 | .take(1) 22 | .map(exercise => !!exercise) 23 | .do(exerciseExists => { 24 | if (!exerciseExists) this.router.navigate(['/builder/exercises']); 25 | }) 26 | .catch(error => { 27 | if (error.status === 404) { 28 | this.router.navigate(['/builder/exercises']); 29 | return Observable.of(false) 30 | } else { 31 | return Observable.throw(error); 32 | } 33 | } 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /trainer/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var connect = require('gulp-connect'); 3 | var PATHS = { 4 | src: 'src/**/*.ts', 5 | html: 'src/**/*.html', 6 | css: 'src/**/*.css' 7 | }; 8 | 9 | gulp.task('clean', function(done) { 10 | var del = require('del'); 11 | del(['dist'], done); 12 | }); 13 | 14 | gulp.task('ts2js', function() { 15 | var typescript = require('gulp-typescript'); 16 | var sourcemaps = require('gulp-sourcemaps'); 17 | 18 | var tsResult = gulp.src(PATHS.src) 19 | .pipe(sourcemaps.init()) 20 | .pipe(typescript({ 21 | noImplicitAny: true, 22 | module: 'system', 23 | target: 'ES5', 24 | moduleResolution: 'node', 25 | emitDecoratorMetadata: true, 26 | experimentalDecorators: true 27 | })); 28 | 29 | return tsResult.js 30 | .pipe(sourcemaps.write()) 31 | .pipe(gulp.dest('dist')) 32 | .pipe(connect.reload()); 33 | }); 34 | 35 | gulp.task('play', ['ts2js'], function() { 36 | var http = require('http'); 37 | var open = require('open'); 38 | var watch = require('gulp-watch'); 39 | 40 | 41 | 42 | var port = 9000, 43 | app; 44 | 45 | connect.server({ 46 | root: __dirname, 47 | port: port, 48 | livereload: true, 49 | fallback: 'index.html' 50 | }); 51 | open('http://localhost:' + port + '/index.html'); 52 | 53 | gulp.watch(PATHS.src, ['ts2js']); 54 | watch(PATHS.html).pipe(connect.reload()); 55 | watch(PATHS.css).pipe(connect.reload()); 56 | }); 57 | -------------------------------------------------------------------------------- /trainer/src/services/model.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable() 4 | export class WorkoutPlan { 5 | constructor( 6 | public name: string, 7 | public title: string, 8 | public restBetweenExercise: number, 9 | public exercises: ExercisePlan[], 10 | public description?: string) { 11 | } 12 | 13 | totalWorkoutDuration(): number { 14 | if (!this.exercises) return 0; 15 | 16 | let total = this.exercises.map((e) => e.duration).reduce((previous, current) => parseInt(previous) + parseInt(current)); 17 | 18 | return ((this.restBetweenExercise ? this.restBetweenExercise : 0) * (this.exercises.length - 1)) + total; 19 | } 20 | } 21 | 22 | @Injectable() 23 | export class ExercisePlan { 24 | constructor(public exercise: Exercise, public duration: any) { 25 | } 26 | } 27 | 28 | export class Exercise { 29 | 30 | constructor( 31 | public name: string, 32 | public title: string, 33 | public description: string, 34 | public image: string, 35 | public nameSound?: string, 36 | public procedure?: string, 37 | public videos?: Array) { } 38 | } 39 | 40 | export class ExerciseProgressEvent { 41 | constructor( 42 | public exercise: ExercisePlan, 43 | public runningFor: number, 44 | public timeRemaining: number, 45 | public workoutTimeRemaining: number) { } 46 | } 47 | 48 | export class ExerciseChangedEvent { 49 | constructor( 50 | public current: ExercisePlan, 51 | public next: ExercisePlan 52 | ) { } 53 | } 54 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout-builder.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { ExerciseGuard } from "./exercise/exercise.guard"; 5 | import { WorkoutGuard } from "./workout/workout.guard"; 6 | 7 | import { WorkoutBuilderComponent} from "./workout-builder.component"; 8 | import { ExerciseComponent } from './exercise/exercise.component'; 9 | import { ExercisesComponent } from './exercises/exercises.component'; 10 | import { WorkoutComponent } from './workout/workout.component'; 11 | import { WorkoutsComponent } from './workouts/workouts.component'; 12 | 13 | export const workoutBuilderRoutes: Routes = [ 14 | { 15 | path: '', 16 | component: WorkoutBuilderComponent, 17 | children: [ 18 | {path:'', pathMatch: 'full', redirectTo: 'workouts'}, 19 | {path:'workouts/workout-not-found', component: WorkoutsComponent }, 20 | {path:'workouts', component: WorkoutsComponent }, 21 | {path:'workout/new', component: WorkoutComponent }, 22 | {path:'workout/:id', component: WorkoutComponent, canActivate: [WorkoutGuard] }, 23 | {path:'exercises', component: ExercisesComponent}, 24 | {path:'exercise/new', component: ExerciseComponent }, 25 | {path:'exercise/:id', component: ExerciseComponent, canActivate: [ExerciseGuard] } 26 | ] 27 | } 28 | ]; 29 | 30 | export const workoutBuilderRouting: ModuleWithProviders = RouterModule.forChild(workoutBuilderRoutes); -------------------------------------------------------------------------------- /guessthenumber/app/guess-the-number.component.ts: -------------------------------------------------------------------------------- 1 | import { Component }from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'my-app', 5 | template: ` 6 |
7 |

Guess the Number !

8 |

Guess the computer generated random number between 1 and 1000.

9 | 10 | 11 | 12 | 13 |
14 |

Your guess is higher.

15 |

Your guess is lower.

16 |

Yes! That's it.

17 |
18 |

No of guesses : 19 | {{noOfTries}} 20 |

21 |
22 | ` 23 | }) 24 | export class GuessTheNumberComponent { 25 | deviation: number; 26 | noOfTries: number; 27 | original: number; 28 | guess: number; 29 | 30 | constructor() { 31 | this.initializeGame(); 32 | } 33 | initializeGame() { 34 | this.noOfTries = 0; 35 | this.original = Math.floor((Math.random() * 1000) + 1); 36 | this.guess = null; 37 | this.deviation = null; 38 | } 39 | verifyGuess() { 40 | this.deviation = this.original - this.guess; 41 | this.noOfTries = this.noOfTries + 1; 42 | } 43 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/video-player/video-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {DialogRef, ModalComponent} from 'angular2-modal'; 3 | import {BSModalContext} from 'angular2-modal/plugins/bootstrap' 4 | import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; 5 | 6 | export class VideoDialogContext extends BSModalContext { 7 | constructor(public videoId: string) { 8 | super(); 9 | this.size = "sm"; 10 | } 11 | } 12 | 13 | // Custom dialog class for showing view in a popup. 14 | @Component({ 15 | selector: 'video-dialog', 16 | template: ` 19 | 22 | `, 25 | }) 26 | export class VideoDialogComponent implements ModalComponent, OnInit { 27 | context: VideoDialogContext; 28 | videoId: SafeResourceUrl; 29 | private youtubeUrlPrefix = '//www.youtube.com/embed/'; 30 | 31 | constructor(public dialog: DialogRef, private sanitizer: DomSanitizer) { } 32 | 33 | ngOnInit() { 34 | this.videoId = this.sanitizer.bypassSecurityTrustResourceUrl(this.youtubeUrlPrefix + this.dialog.context.videoId); 35 | } 36 | 37 | ok() { 38 | this.dialog.close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /trainer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trainer", 3 | "version": "1.0.0", 4 | "description": "Angular 2 implementation of Personal Trainer in TypeScript", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/chandermani/angular2byexample.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/chandermani/angular2byexample/issues" 11 | }, 12 | "homepage": "https://github.com/chandermani/angular2byexample.git#readme", 13 | "devDependencies": { 14 | "@types/core-js": "0.9.36", 15 | "@types/jasmine": "^2.5.43", 16 | "@types/selenium-webdriver": "^3.0.0", 17 | "del": "^1.2.0", 18 | "gulp": "^3.9.0", 19 | "gulp-connect": "^3.1.0", 20 | "gulp-sourcemaps": "^1.6.0", 21 | "gulp-typescript": "^3.1.5", 22 | "gulp-watch": "^4.3.5", 23 | "gulp-webserver": "^0.9.1", 24 | "jasmine-core": "2.4.1", 25 | "karma": "^0.13.22", 26 | "karma-chrome-launcher": "^0.2.3", 27 | "karma-jasmine": "^0.3.8", 28 | "open": "0.0.5", 29 | "protractor": "^3.3.0", 30 | "typescript": "^2.2.1" 31 | }, 32 | "dependencies": { 33 | "@angular/common": "2.0.0", 34 | "@angular/compiler": "2.0.0", 35 | "@angular/core": "2.0.0", 36 | "@angular/forms": "2.0.0", 37 | "@angular/http": "2.0.0", 38 | "@angular/platform-browser": "2.0.0", 39 | "@angular/platform-browser-dynamic": "2.0.0", 40 | "@angular/router": "3.0.0", 41 | "angular2-modal": "2.0.0-beta.13", 42 | "core-js": "^2.4.1", 43 | "reflect-metadata": "0.1.3", 44 | "rxjs": "5.0.0-beta.12", 45 | "systemjs": "0.19.27", 46 | "zone.js": "^0.6.23" 47 | }, 48 | "scripts": {} 49 | } 50 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/builder-services/exercise-builder-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { Exercise, ExercisePlan, WorkoutPlan } from '../../../services/model'; 4 | import { WorkoutService } from "../../../services/workout-service"; 5 | 6 | @Injectable() 7 | export class ExerciseBuilderService { 8 | buildingExercise: Exercise; 9 | newExercise: boolean; 10 | 11 | constructor(public workoutService:WorkoutService){} 12 | 13 | startBuildingNew(){ 14 | this.buildingExercise = new Exercise("", "", "", ""); 15 | this.newExercise = true; 16 | return this.buildingExercise; 17 | } 18 | 19 | startBuildingExisting(name: string){ 20 | this.newExercise = false; 21 | return this.workoutService.getExercise(name) 22 | } 23 | 24 | save(){ 25 | let exercise = this.newExercise ? 26 | this.workoutService.addExercise(this.buildingExercise) : 27 | this.workoutService.updateExercise(this.buildingExercise); 28 | this.newExercise = false; 29 | return exercise; 30 | } 31 | 32 | delete(){ 33 | this.workoutService.deleteExercise(this.buildingExercise.name); 34 | } 35 | 36 | addVideo(){ 37 | if(!this.buildingExercise.videos) 38 | { 39 | this.buildingExercise.videos = []; 40 | } 41 | this.buildingExercise.videos.push(""); 42 | } 43 | 44 | canDeleteExercise(){ 45 | return !this.newExercise; 46 | } 47 | 48 | deleteVideo(index: number){ 49 | if (index >= 0) this.buildingExercise.videos.splice(index, 1); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /trainer/src/components/start/start.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Ready for a Workout?

4 |
5 | 6 |
7 | 13 | 19 |
20 |
21 |

Workouts

22 |
23 |
24 | 26 |
27 |
28 |
30 |
{{workout.title}}
31 |
32 | - {{ workout.totalWorkoutDuration()|secondsToTime}} 33 | - {{workout.exercises.length}} 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-runner.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |

Workout Remaining - {{workoutTimeRemaining | secondsToTime}}

12 |

{{currentExercise.exercise.title}}

13 | 14 |
15 |
18 |
19 |
20 |
21 |

Time Remaining: 22 | {{currentExercise.duration-exerciseRunningDuration}} 23 |

24 |

Next up: 25 | {{workoutPlan.exercises[currentExerciseIndex + 1].exercise.title}} 26 |

27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | -------------------------------------------------------------------------------- /trainer/src/components/workout-history/workout-history.html: -------------------------------------------------------------------------------- 1 |
2 |

Workout History

3 |
4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
NoStartedEndedLast ExerciseExercises DoneCompleted
{{i+1}}{{historyItem.startedOn | date:'short'}}{{historyItem.endedOn | date:'short'}}{{historyItem.lastExercise}}{{historyItem.exercisesDone}}{{historyItem.completed ? "Yes" : "No"}}
No Workout History Found.
37 |
38 |
39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/builder-services/workout-builder-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WorkoutPlan, Exercise } from '../../../services/model'; 3 | import { WorkoutService } from "../../../services/workout-service"; 4 | import { ExercisePlan } from "../../../services/model"; 5 | 6 | @Injectable() 7 | export class WorkoutBuilderService { 8 | buildingWorkout: WorkoutPlan; 9 | newWorkout: boolean; 10 | firstExercise: boolean = true; 11 | 12 | constructor(public workoutService:WorkoutService){} 13 | 14 | startBuildingNew(){ 15 | let exerciseArray : ExercisePlan[] = []; 16 | this.buildingWorkout = new WorkoutPlan("", "", 30, exerciseArray); 17 | this.newWorkout = true; 18 | return this.buildingWorkout; 19 | } 20 | 21 | startBuildingExisting(name: string){ 22 | this.newWorkout = false; 23 | return this.workoutService.getWorkout(name); 24 | } 25 | 26 | removeExercise(exercise: ExercisePlan){ 27 | var currentIndex = this.buildingWorkout.exercises.map(function(e: any) { return e.exercise.name; }).indexOf(exercise.exercise.name); 28 | this.buildingWorkout.exercises.splice(currentIndex, 1) 29 | } 30 | 31 | addExercise(exercisePlan: ExercisePlan){ 32 | if(this.newWorkout && this.firstExercise){ 33 | this.buildingWorkout.exercises.splice(0, 1); 34 | this.firstExercise = false; 35 | } 36 | this.buildingWorkout.exercises.push(exercisePlan); 37 | } 38 | 39 | moveExerciseTo(exercise: ExercisePlan, toIndex: number ){ 40 | if (toIndex < 0 || toIndex >= this.buildingWorkout.exercises.length) return; 41 | var currentIndex = this.buildingWorkout.exercises.indexOf(exercise); 42 | this.buildingWorkout.exercises.splice(toIndex, 0, this.buildingWorkout.exercises.splice(currentIndex, 1)[0]); 43 | } 44 | 45 | save(){ 46 | let workout = this.newWorkout ? 47 | this.workoutService.addWorkout(this.buildingWorkout) : 48 | this.workoutService.updateWorkout(this.buildingWorkout); 49 | this.newWorkout = false; 50 | return workout; 51 | } 52 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-audio/workout-audio.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild, Inject, forwardRef} from '@angular/core'; 2 | import {MyAudioDirective} from './my-audio.directive' 3 | import {WorkoutPlan, ExercisePlan, ExerciseProgressEvent, ExerciseChangedEvent} from '../../../services/model'; 4 | 5 | @Component({ 6 | selector: 'workout-audio', 7 | templateUrl: '/src/components/workout-runner/workout-audio/workout-audio.html' 8 | }) 9 | export class WorkoutAudioComponent { 10 | @ViewChild('ticks') private ticks: MyAudioDirective; 11 | @ViewChild('nextUp') private nextUp: MyAudioDirective; 12 | @ViewChild('nextUpExercise') private nextUpExercise: MyAudioDirective; 13 | @ViewChild('halfway') private halfway: MyAudioDirective; 14 | @ViewChild('aboutToComplete') private aboutToComplete: MyAudioDirective; 15 | private nextupSound: string; 16 | 17 | stop() { 18 | this.ticks.stop(); 19 | this.nextUp.stop(); 20 | this.halfway.stop(); 21 | this.aboutToComplete.stop(); 22 | this.nextUpExercise.stop(); 23 | } 24 | 25 | resume() { 26 | this.ticks.start(); 27 | if (this.nextUp.currentTime > 0 && !this.nextUp.playbackComplete) this.nextUp.start(); 28 | else if (this.nextUpExercise.currentTime > 0 && !this.nextUpExercise.playbackComplete) this.nextUpExercise.start(); 29 | else if (this.halfway.currentTime > 0 && !this.halfway.playbackComplete) this.halfway.start(); 30 | else if (this.aboutToComplete.currentTime > 0 && !this.aboutToComplete.playbackComplete) this.aboutToComplete.start(); 31 | } 32 | 33 | onExerciseProgress(progress: ExerciseProgressEvent) { 34 | if (progress.runningFor == Math.floor(progress.exercise.duration / 2) 35 | && progress.exercise.exercise.name != "rest") { 36 | this.halfway.start(); 37 | } 38 | else if (progress.timeRemaining == 3) { 39 | this.aboutToComplete.start(); 40 | } 41 | } 42 | 43 | onExerciseChanged(state: ExerciseChangedEvent) { 44 | if (state.current.exercise.name == "rest") { 45 | this.nextupSound = state.next.exercise.nameSound; 46 | setTimeout(() => this.nextUp.start(), 2000); 47 | setTimeout(() => this.nextUpExercise.start(), 3000); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /trainer/src/services/workout-history-tracker.ts: -------------------------------------------------------------------------------- 1 | import {ExercisePlan} from './model'; 2 | import {LocalStorage} from './local-storage'; 3 | import {Injectable} from '@angular/core'; 4 | 5 | @Injectable() 6 | export class WorkoutHistoryTracker { 7 | private maxHistoryItems: number = 20; //We only track for last 20 exercise 8 | private currentWorkoutLog: WorkoutLogEntry = null; 9 | private workoutHistory: Array = []; 10 | private workoutTracked: boolean; 11 | private storageKey: string = "workouts"; 12 | 13 | constructor(private storage: LocalStorage) { 14 | this.workoutHistory = (storage.getItem>(this.storageKey) || []) 15 | .map((item: WorkoutLogEntry) => { 16 | item.startedOn = new Date(item.startedOn.toString()); 17 | item.endedOn = item.endedOn == null ? null : new Date(item.endedOn.toString()); 18 | return item; 19 | }); 20 | } 21 | 22 | get tracking(): boolean { 23 | return this.workoutTracked; 24 | } 25 | 26 | startTracking() { 27 | this.workoutTracked = true; 28 | this.currentWorkoutLog = new WorkoutLogEntry(new Date()); 29 | if (this.workoutHistory.length >= this.maxHistoryItems) { 30 | this.workoutHistory.shift(); 31 | } 32 | this.workoutHistory.push(this.currentWorkoutLog); 33 | this.storage.setItem(this.storageKey, this.workoutHistory); 34 | } 35 | 36 | exerciseComplete(exercise: ExercisePlan) { 37 | this.currentWorkoutLog.lastExercise = exercise.exercise.title; 38 | ++this.currentWorkoutLog.exercisesDone; 39 | this.storage.setItem(this.storageKey, this.workoutHistory); 40 | } 41 | 42 | endTracking(completed: boolean) { 43 | this.currentWorkoutLog.completed = completed; 44 | this.currentWorkoutLog.endedOn = new Date(); 45 | this.currentWorkoutLog = null; 46 | this.workoutTracked = false; 47 | this.storage.setItem(this.storageKey, this.workoutHistory); 48 | }; 49 | 50 | getHistory(): Array { 51 | return this.workoutHistory; 52 | } 53 | } 54 | export class WorkoutLogEntry { 55 | constructor( 56 | public startedOn: Date, 57 | public completed: boolean = false, 58 | public exercisesDone: number = 0, 59 | public lastExercise?: string, 60 | public endedOn?: Date) { } 61 | } 62 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout-builder.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule} from '@angular/common'; 3 | import { SharedModule } from "../shared/shared.module"; 4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 5 | 6 | import { ExerciseBuilderService } from "./builder-services/exercise-builder-service"; 7 | import { ExerciseComponent } from "./exercise/exercise.component"; 8 | import { ExercisesComponent } from "./exercises/exercises.component"; 9 | import { ExerciseGuard } from "./exercise/exercise.guard"; 10 | import { LeftNavExercisesComponent } from "./navigation/left-nav-exercises.component"; 11 | import { LeftNavMainComponent } from "./navigation/left-nav-main.component"; 12 | import { SubNavComponent} from './navigation/sub-nav.component'; 13 | import { WorkoutBuilderComponent } from "./workout-builder.component"; 14 | import { WorkoutComponent } from "./workout/workout.component"; 15 | import { WorkoutsComponent } from "./workouts/workouts.component"; 16 | import { WorkoutGuard } from './workout/workout.guard'; 17 | import { WorkoutBuilderService } from "./builder-services/workout-builder-service"; 18 | 19 | import { workoutBuilderRouting } from './workout-builder.routes'; 20 | import { BusyIndicatorDirective } from "./shared/busy-indicator.directive"; 21 | import { RemoteValidatorDirective } from "./shared/remote-validator.directive"; 22 | import {AjaxButtonComponent} from "./shared/ajax-button.component"; 23 | 24 | 25 | @NgModule({ 26 | imports: [ 27 | CommonModule, 28 | FormsModule, 29 | ReactiveFormsModule, 30 | SharedModule, 31 | workoutBuilderRouting 32 | ], 33 | declarations: [ 34 | WorkoutBuilderComponent, 35 | WorkoutComponent, 36 | WorkoutsComponent, 37 | ExerciseComponent, 38 | ExercisesComponent, 39 | SubNavComponent, 40 | LeftNavExercisesComponent, 41 | LeftNavMainComponent, 42 | RemoteValidatorDirective, 43 | BusyIndicatorDirective, 44 | AjaxButtonComponent 45 | ], 46 | providers: [ 47 | ExerciseBuilderService, 48 | ExerciseGuard, 49 | WorkoutBuilderService, 50 | WorkoutGuard 51 | ] 52 | }) 53 | export class WorkoutBuilderModule { } -------------------------------------------------------------------------------- /trainer/systemjs.config.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | 3 | // map tells the System loader where to look for things 4 | var map = { 5 | 'app': 'dist', // 'dist', 6 | 'rxjs': 'node_modules/rxjs', 7 | '@angular': 'node_modules/@angular', 8 | 'angular2-modal': 'node_modules/angular2-modal', 9 | 'angular2-modal/platform-browser': 'node_modules/angular2-modal/platform-browser', 10 | 'angular2-modal/plugins/bootstrap': 'node_modules/angular2-modal/plugins/bootstrap' 11 | }; 12 | 13 | // packages tells the System loader how to load when no filename and/or no extension 14 | var packages = { 15 | 'app': { main: 'bootstrap.js', defaultExtension: 'js' }, 16 | 'rxjs': { defaultExtension: 'js' }, 17 | 'angular2-modal': {main: 'index.js', defaultExtension: 'js'}, 18 | 'angular2-modal/platform-browser': {main: 'index.js', defaultExtension: 'js'}, 19 | 'angular2-modal/plugins/bootstrap': {main: 'index.js', defaultExtension: 'js'}, 20 | }; 21 | 22 | var ngPackageNames = [ 23 | 'common', 24 | 'compiler', 25 | 'core', 26 | 'forms', 27 | 'http', 28 | 'platform-browser', 29 | 'platform-browser-dynamic', 30 | 'router', 31 | 'testing' 32 | ]; 33 | 34 | // Individual files (~300 requests): 35 | function packIndex(pkgName) { 36 | packages['@angular/' + pkgName] = { main: 'index.js', defaultExtension: 'js' }; 37 | } 38 | 39 | // add package entries for angular packages in the form '@angular/common': { main: 'index.js', defaultExtension: 'js' } 40 | // Bundled (~40 requests): 41 | function packUmd(pkgName) { 42 | packages['@angular/' + pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' }; 43 | } 44 | 45 | // Most environments should use UMD; some (Karma) need the individual index files 46 | var setPackageConfig = System.packageWithIndex ? packIndex : packUmd; 47 | 48 | // Add package entries for angular packages 49 | ngPackageNames.forEach(setPackageConfig); 50 | 51 | var config = { 52 | map: map, 53 | packages: packages 54 | } 55 | 56 | // filterSystemConfig - index.html's chance to modify config before we register it. 57 | if (global.filterSystemConfig) { global.filterSystemConfig(config); } 58 | 59 | System.config(config); 60 | 61 | })(this); -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-audio/workout-audio0.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild, Inject, forwardRef} from '@angular/core'; 2 | import {MyAudioDirective} from './my-audio.directive' 3 | import {WorkoutRunnerComponent} from '../workout-runner.component' 4 | import {WorkoutPlan, ExercisePlan, ExerciseProgressEvent, ExerciseChangedEvent} from '../../../services/model'; 5 | 6 | @Component({ 7 | selector: 'workout-audio', 8 | templateUrl: '/src/components/workout-runner/workout-audio/workout-audio.html' 9 | }) 10 | export class WorkoutAudioComponent { 11 | @ViewChild('ticks') private ticks: MyAudioDirective; 12 | @ViewChild('nextUp') private nextUp: MyAudioDirective; 13 | @ViewChild('nextUpExercise') private nextUpExercise: MyAudioDirective; 14 | @ViewChild('halfway') private halfway: MyAudioDirective; 15 | @ViewChild('aboutToComplete') private aboutToComplete: MyAudioDirective; 16 | private nameSounds: Array; 17 | private nextupSound: string; 18 | private subscriptions: Array; 19 | 20 | constructor( @Inject(forwardRef(() => WorkoutRunnerComponent)) private runner: WorkoutRunnerComponent) { 21 | this.subscriptions = [ 22 | this.runner.exercisePaused.subscribe((exercise: ExercisePlan) => this.stop()), 23 | this.runner.workoutComplete.subscribe((exercise: ExercisePlan) => this.stop()), 24 | this.runner.exerciseResumed.subscribe((exercise: ExercisePlan) => this.resume()), 25 | this.runner.exerciseProgress.subscribe((progress: ExerciseProgressEvent) => this.onExerciseProgress(progress)), 26 | this.runner.exerciseChanged.subscribe((state: ExerciseChangedEvent) => this.onExerciseChanged(state))] 27 | } 28 | 29 | stop() { 30 | this.ticks.stop(); 31 | this.nextUp.stop(); 32 | this.halfway.stop(); 33 | this.aboutToComplete.stop(); 34 | this.nextUpExercise.stop(); 35 | } 36 | 37 | resume() { 38 | this.ticks.start(); 39 | if (this.nextUp.currentTime > 0 && !this.nextUp.playbackComplete) this.nextUp.start(); 40 | else if (this.nextUpExercise.currentTime > 0 && !this.nextUpExercise.playbackComplete) this.nextUpExercise.start(); 41 | else if (this.halfway.currentTime > 0 && !this.halfway.playbackComplete) this.halfway.start(); 42 | else if (this.aboutToComplete.currentTime > 0 && !this.aboutToComplete.playbackComplete) this.aboutToComplete.start(); 43 | } 44 | 45 | private onExerciseProgress(exercise: any) { 46 | if (exercise.runningFor == Math.floor(exercise.exercise.duration / 2) 47 | && exercise.exercise.exercise.name != "rest") { 48 | this.halfway.start(); 49 | } 50 | else if (exercise.timeRemaining == 3) { 51 | this.aboutToComplete.start(); 52 | } 53 | } 54 | 55 | private onExerciseChanged(state: any) { 56 | if (state.current.exercise.name == "rest") { 57 | this.nextupSound = state.next.exercise.nameSound; 58 | setTimeout(() => this.nextUp.start(), 2000); 59 | setTimeout(() => this.nextUpExercise.start(), 3000); 60 | } 61 | } 62 | 63 | ngOnDestroy() { 64 | this.subscriptions.forEach((s) => s.unsubscribe()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/shared/remote-validator.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component, Directive, NO_ERRORS_SCHEMA, DebugElement} from '@angular/core'; 2 | import { TestBed, fakeAsync, async, tick } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | import { FormsModule, NG_VALIDATORS, AbstractControl, NgForm, FormControl } from '@angular/forms'; 5 | 6 | import { RemoteValidatorDirective } from "./remote-validator.directive"; 7 | 8 | @Component({ 9 | template: ` 10 |
11 | 13 |
14 | ` 15 | }) 16 | export class TestComponent { 17 | workoutName: string; 18 | 19 | constructor() { 20 | this.workoutName = '7MinWorkout'; 21 | } 22 | validateWorkoutName = (name: string): Promise => { 23 | return Promise.resolve(false); 24 | } 25 | } 26 | 27 | describe('RemoteValidator', () => { 28 | let fixture: any; 29 | let comp: any; 30 | let debug: any; 31 | let input: any; 32 | 33 | beforeEach(async(() => { 34 | TestBed.configureTestingModule({ 35 | imports: [ FormsModule ], 36 | declarations: [ TestComponent, RemoteValidatorDirective ] 37 | }); 38 | fixture = TestBed.createComponent(TestComponent); 39 | comp = fixture.componentInstance; 40 | debug = fixture.debugElement; 41 | input = debug.query(By.css('[name=workoutName]')); 42 | })); 43 | 44 | it("should load the directive without error", fakeAsync(() => { 45 | expect(input.attributes.a2beRemoteValidator).toBe('workoutname', 'remote validator directive should be loaded.') 46 | })); 47 | 48 | it('should create error if remote validation fails', fakeAsync(() => { 49 | spyOn(comp, 'validateWorkoutName').and.callThrough(); 50 | fixture.detectChanges(); 51 | input.nativeElement.value = '6MinWorkout'; 52 | tick(); 53 | 54 | let form: NgForm = debug.children[0].injector.get(NgForm); 55 | let control = form.control.get('workoutName'); 56 | 57 | expect(comp.validateWorkoutName).toHaveBeenCalled(); 58 | expect(control.hasError('workoutname')).toBe(true); 59 | expect(control.valid).toBe(false); 60 | expect(form.valid).toEqual(false); 61 | expect(form.control.valid).toEqual(false); 62 | expect(form.control.hasError('workoutname', ['workoutName'])).toEqual(true); 63 | })); 64 | 65 | it('should not create error if remote validation succeeds', fakeAsync(() => { 66 | spyOn(comp, 'validateWorkoutName').and.returnValue(Promise.resolve(true)); 67 | fixture.detectChanges(); 68 | input.nativeElement.value = '6MinWorkout'; 69 | tick(); 70 | 71 | let form: NgForm = debug.children[0].injector.get(NgForm); 72 | let control = form.control.get('workoutName'); 73 | 74 | expect(comp.validateWorkoutName).toHaveBeenCalled(); 75 | expect(control.hasError('workoutname')).toBe(false); 76 | expect(control.valid).toBe(true); 77 | expect(form.control.valid).toEqual(true); 78 | expect(form.valid).toEqual(true); 79 | expect(form.control.hasError('workoutname', ['workoutName'])).toEqual(false); 80 | })); 81 | }); -------------------------------------------------------------------------------- /trainer/tests/e2e/workout-runner.e2e.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | class WorkoutRunnerPage{ 4 | pauseResume: any; 5 | playButton: any; 6 | pauseButton: any; 7 | exerciseTitle: any; 8 | exerciseDescription: any; 9 | exerciseTimeRemaining; any; 10 | 11 | constructor(){ 12 | this.pauseResume = element.all(by.id('pause-overlay')); 13 | this.playButton = element.all(by.css('.glyphicon-play')); 14 | this.pauseButton = element.all(by.css('.glyphicon-pause')); 15 | this.exerciseTitle = element.all(by.css('.workout-display-div h1')).getAttribute('value'); 16 | this.exerciseDescription = element.all(by.id('description-panel')).getAttribute('value'); 17 | this.exerciseTimeRemaining = element.all(by.css('.workout-display-div h4')).getAttribute('value'); 18 | } 19 | }; 20 | 21 | describe("Workout Runner", () => { 22 | 23 | describe("Start Page", () => { 24 | beforeEach(() => { 25 | browser.get(""); 26 | }); 27 | it("should load the start page.", () => { 28 | expect(browser.getTitle()).toBe("Personal Trainer"); 29 | expect(element(by.id("start")).getText()).toBe("Select Workout"); 30 | }); 31 | 32 | it("should search workout with specific name.", () => { 33 | var filteredWorkouts = element.all(by.css(".workout.tile")); 34 | expect(filteredWorkouts.count()).toEqual(2); 35 | 36 | var searchInput = element(by.css(".form-control")); 37 | searchInput.sendKeys("1 Minute Workout"); 38 | 39 | expect(filteredWorkouts.count()).toEqual(1); 40 | expect(filteredWorkouts.first().element(by.css(".title")).getText()).toBe("1 Minute Workout"); 41 | }); 42 | 43 | it("should navigate to workout runner.", () => { 44 | var filteredWorkouts = element.all(by.css(".workout.tile")); 45 | filteredWorkouts.first().click(); 46 | expect(browser.getCurrentUrl()).toContain("/workout/1minworkout"); 47 | }); 48 | }); 49 | 50 | describe("Workout Runner page", () => { 51 | beforeEach(() => { 52 | browser.get("#/workout/1minworkout"); 53 | }); 54 | 55 | it("should load workout data", () => { 56 | var page = new WorkoutRunnerPage(); 57 | page.pauseResume.click(); 58 | expect(page.exerciseTitle).toBe['Jumping Jacks']; 59 | expect(page.exerciseDescription).toBe["A jumping jack or star jump, also called side-straddle hop is a physical jumping exercise."]; 60 | }); 61 | 62 | it("should pause workout when paused button clicked", () => { 63 | let page = new WorkoutRunnerPage(), 64 | timeRemaining; 65 | 66 | page.pauseResume.click(); 67 | expect(page.playButton.count()).toBe(1); 68 | expect(page.pauseButton.count()).toBe(0); 69 | 70 | page.exerciseTimeRemaining.then((time)=> { 71 | timeRemaining = time; 72 | browser.sleep(3000); 73 | }); 74 | page.exerciseTimeRemaining.then((time)=> { 75 | expect(page.exerciseTimeRemaining).toBe(timeRemaining); 76 | }); 77 | }); 78 | 79 | it("should transition exercise when time lapses.", () => { 80 | var page = new WorkoutRunnerPage(); 81 | browser.sleep(15000); 82 | page.pauseResume.click(); 83 | expect(page.exerciseTitle).toBe["Relax!"]; 84 | expect(page.exerciseDescription).toBe["Relax a bit!"]; 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /trainer/tests/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | // Tun on full stack traces in errors to help debugging 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | // // Cancel Karma's synchronous start, 6 | // // we will call `__karma__.start()` later, once all the specs are loaded. 7 | __karma__.loaded = function () { 8 | }; 9 | function isJsFile(path) { 10 | return path.slice(-3) == '.js'; 11 | } 12 | 13 | function isSpecFile(path) { 14 | return /\.spec\.js$/.test(path); 15 | } 16 | 17 | function isBuiltFile(path) { 18 | var builtPath = '/base/dist/'; 19 | return isJsFile(path) && (path.substr(0, builtPath.length) == builtPath); 20 | } 21 | 22 | var allSpecFiles = Object.keys(window.__karma__.files) 23 | .filter(isSpecFile) 24 | .filter(isBuiltFile); 25 | 26 | // Load our SystemJS configuration. 27 | System.config({ 28 | baseURL: '/base' 29 | }); 30 | 31 | System.config( 32 | { 33 | paths: { 34 | // paths serve as alias 35 | 'npm:': 'node_modules/' 36 | }, 37 | map: { 38 | 'app': 'dist', 39 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 40 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 41 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 42 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 43 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 44 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 45 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 46 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 47 | 48 | // angular testing umd bundles 49 | '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js', 50 | '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js', 51 | '@angular/forms/testing': 'npm:@angular/formsn/bundles/forms-testing.umd.js', 52 | '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js', 53 | '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js', 54 | '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js', 55 | '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js', 56 | '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', 57 | 58 | // other libraries 59 | 'rxjs': 'npm:rxjs', 60 | 'angular2-modal': 'npm:angular2-modal', 61 | }, 62 | packages: { 63 | 'app': { 64 | defaultExtension: 'js' 65 | }, 66 | 'rxjs': { 67 | defaultExtension: 'js' 68 | } 69 | } 70 | }); 71 | 72 | Promise.all([ 73 | System.import('@angular/core/testing'), 74 | System.import('@angular/platform-browser-dynamic/testing') 75 | ]).then(function (providers) { 76 | var testing = providers[0]; 77 | var testingBrowser = providers[1]; 78 | 79 | testing.TestBed.initTestEnvironment(testingBrowser.BrowserDynamicTestingModule, 80 | testingBrowser.platformBrowserDynamicTesting()); 81 | 82 | }).then(function() { 83 | // Finally, load all spec files. 84 | // This will run the tests directly. 85 | return Promise.all( 86 | allSpecFiles.map(function (moduleName) { 87 | return System.import(moduleName); 88 | })); 89 | }).then(__karma__.start, __karma__.error); -------------------------------------------------------------------------------- /trainer/tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Mar 12 2016 19:12:47 GMT-0500 (Eastern Standard Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '../', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['jasmine'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | // Polyfills. 17 | 'node_modules/core-js/client/shim.js', 18 | 'node_modules/reflect-metadata/Reflect.js', 19 | 20 | // System.js for module loading 21 | 'node_modules/systemjs/dist/system-polyfills.js', 22 | 'node_modules/systemjs/dist/system.src.js', 23 | 24 | // Zone.js dependencies 25 | 'node_modules/zone.js/dist/zone.js', 26 | 'node_modules/zone.js/dist/proxy.js', 27 | 'node_modules/zone.js/dist/sync-test.js', 28 | 'node_modules/zone.js/dist/jasmine-patch.js', 29 | 'node_modules/zone.js/dist/async-test.js', 30 | 'node_modules/zone.js/dist/fake-async-test.js', 31 | 32 | // RxJs. 33 | { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, 34 | { pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false }, 35 | 36 | // karma 37 | {pattern: 'tests/karma-test-shim.js', included: true, watched: true}, 38 | //{pattern: 'src/test/matchers.js', included: true, watched: true}, 39 | 40 | // paths loaded via module imports 41 | // Angular itself 42 | {pattern: 'node_modules/@angular/**/*.js', included: false, watched: true}, 43 | {pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: true}, 44 | 45 | // Angular 2 modal 46 | {pattern: 'node_modules/angular2-modal/**/*.js', included: false, watched: true}, 47 | 48 | // Our built application code 49 | {pattern: 'dist/**/*.js', included: false, watched: true}, 50 | 51 | // paths loaded via Angular's component compiler 52 | // (these paths need to be rewritten, see proxies section) 53 | {pattern: 'src/**/*.html', included: false, watched: true}, 54 | {pattern: 'static/**/*.css', included: false, watched: true}, 55 | 56 | // paths to support debugging with source maps in dev tools 57 | {pattern: 'src/**/*.ts', included: false, watched: false}, 58 | {pattern: 'node_modules/**/*.js.map', included: false, watched: false} 59 | ], 60 | 61 | // list of files to exclude 62 | exclude: [ 63 | ], 64 | 65 | // proxied base paths 66 | proxies: { 67 | // required for component assests fetched by Angular's compiler 68 | "/src/": "/base/src/" 69 | }, 70 | 71 | // preprocess matching files before serving them to the browser 72 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 73 | preprocessors: { 74 | }, 75 | 76 | // test results reporter to use 77 | // possible values: 'dots', 'progress' 78 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 79 | reporters: ['progress'], 80 | 81 | // web server port 82 | port: 9876, 83 | 84 | // enable / disable colors in the output (reporters and logs) 85 | colors: true, 86 | 87 | // level of logging 88 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 89 | logLevel: config.LOG_INFO, 90 | 91 | // enable / disable watching file and executing tests whenever any file changes 92 | autoWatch: true, 93 | 94 | // start these browsers 95 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 96 | browsers: ['Chrome'], 97 | 98 | // Continuous Integration mode 99 | // if true, Karma captures browsers, runs the tests and exits 100 | singleRun: false, 101 | 102 | // Concurrency level 103 | // how many browser should be started simultaneous 104 | concurrency: Infinity 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout/workout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | 4 | import { WorkoutPlan, ExercisePlan } from "../../../services/model"; 5 | import { WorkoutBuilderService } from "../builder-services/workout-builder-service"; 6 | 7 | import { WorkoutService } from "../../../services/workout-service"; 8 | 9 | @Component({ 10 | selector: 'workout', 11 | templateUrl: '/src/components/workout-builder/workout/workout.component.html' 12 | }) 13 | 14 | export class WorkoutComponent implements OnInit, OnDestroy{ 15 | workout: WorkoutPlan; 16 | sub: any; 17 | submitted: boolean = false; 18 | removeTouched: boolean = false; 19 | isExistingWorkout: boolean = false; 20 | private workoutName: string; 21 | 22 | constructor( 23 | public route: ActivatedRoute, 24 | public router: Router, 25 | private workoutBuilderService:WorkoutBuilderService, 26 | private workoutService: WorkoutService){ } 27 | 28 | ngOnInit() { 29 | this.sub = this.route.params.subscribe(params => { 30 | if (!params['id']) { 31 | this.workout = this.workoutBuilderService.startBuildingNew(); 32 | } else { 33 | this.workoutName = params['id']; 34 | this.isExistingWorkout = true; 35 | this.workoutBuilderService.startBuildingExisting(this.workoutName) 36 | .subscribe( 37 | (data: WorkoutPlan) => { 38 | this.workout = data; 39 | if (!this.workout) { 40 | this.router.navigate(['/builder/workouts/workout-not-found']); 41 | } else { 42 | this.workoutBuilderService.buildingWorkout = this.workout; 43 | } 44 | }, 45 | (err: any) => { 46 | if (err.status === 404) { 47 | this.router.navigate(['/builder/workouts/workout-not-found']) 48 | } else { 49 | console.error(err) 50 | } 51 | } 52 | ); 53 | } 54 | }); 55 | } 56 | 57 | addExercise(exercisePlan: ExercisePlan) { 58 | this.workoutBuilderService.addExercise(exercisePlan); 59 | } 60 | 61 | moveExerciseTo(exercisePlan: ExercisePlan, location: any) { 62 | this.workoutBuilderService.moveExerciseTo(exercisePlan, location); 63 | } 64 | 65 | removeExercise(exercisePlan: ExercisePlan) { 66 | this.removeTouched = true; 67 | this.workoutBuilderService.removeExercise(exercisePlan); 68 | } 69 | 70 | save = (formWorkout: any): Promise => { 71 | this.submitted = true; 72 | if (!formWorkout.valid) return; 73 | let savePromise = this.workoutBuilderService.save().toPromise(); 74 | savePromise.then( 75 | (data) => this.router.navigate(['/builder/workouts']), 76 | (err) => console.error(err) 77 | ); 78 | return savePromise; 79 | } 80 | 81 | ngOnDestroy() { 82 | this.sub.unsubscribe(); 83 | } 84 | 85 | validateWorkoutName = (name: string): Promise => { 86 | if (this.workoutName === name) return Promise.resolve(true); 87 | return this.workoutService.getWorkout(name) 88 | .toPromise() 89 | .then((workout: WorkoutPlan) => { 90 | return !workout; 91 | }, error => { 92 | return true; 93 | }); 94 | } 95 | 96 | durations = [{ title: "15 seconds", value: 15 }, 97 | { title: "30 seconds", value: 30 }, 98 | { title: "45 seconds", value: 45 }, 99 | { title: "1 minute", value: 60 }, 100 | { title: "1 minute 15 seconds", value: 75 }, 101 | { title: "1 minute 30 seconds", value: 90 }, 102 | { title: "1 minute 45 seconds", value: 105 }, 103 | { title: "2 minutes", value: 120 }, 104 | { title: "2 minutes 15 seconds", value: 135 }, 105 | { title: "2 minutes 30 seconds", value: 150 }, 106 | { title: "2 minutes 45 seconds", value: 165 }, 107 | { title: "3 minutes", value: 180 }, 108 | { title: "3 minutes 15 seconds", value: 195 }, 109 | { title: "3 minutes 30 seconds", value: 210 }, 110 | { title: "3 minutes 45 seconds", value: 225 }, 111 | { title: "4 minutes", value: 240 }, 112 | { title: "4 minutes 15 seconds", value: 255 }, 113 | { title: "4 minutes 30 seconds", value: 270 }, 114 | { title: "4 minutes 45 seconds", value: 285 }, 115 | { title: "5 minutes", value: 300 }]; 116 | } -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/exercise/exercise.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | {{exerciseForm.controls.title.value||'Exercise Title*'}} 6 |

7 |
8 |
9 |
10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 |
42 |
43 | 44 |
45 | 48 | 49 | 50 |
51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 |
-------------------------------------------------------------------------------- /trainer/src/components/workout-builder/exercise/exercise.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, DoCheck } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Validators, FormArray, FormGroup, FormControl, FormBuilder } from '@angular/forms'; 4 | 5 | import { ExerciseBuilderService } from "../builder-services/exercise-builder-service"; 6 | import { AlphaNumericValidator } from "../alphanumeric-validator"; 7 | import { Exercise} from "../../../services/model"; 8 | 9 | @Component({ 10 | selector: 'exercise', 11 | templateUrl: '/src/components/workout-builder/exercise/exercise.component.html', 12 | }) 13 | 14 | export class ExerciseComponent implements OnInit, OnDestroy, DoCheck{ 15 | exercise: Exercise; 16 | submitted: boolean = false; 17 | exerciseForm: FormGroup; 18 | model: any; 19 | video: any; 20 | sub: any; 21 | videoArray: FormArray = new FormArray([]); 22 | dataLoaded: boolean = false; 23 | 24 | constructor( 25 | public route: ActivatedRoute, 26 | public router: Router, 27 | public exerciseBuilderService:ExerciseBuilderService, 28 | public formBuilder: FormBuilder 29 | ){} 30 | 31 | ngOnInit(): any { 32 | this.sub = this.route.params.subscribe(params => { 33 | if (!params['id']) { 34 | this.exercise = this.exerciseBuilderService.startBuildingNew(); 35 | } else { 36 | let exerciseName = params['id']; 37 | this.exerciseBuilderService.startBuildingExisting(exerciseName) 38 | .subscribe( 39 | (data:Exercise) => { 40 | this.exercise = data; 41 | if (!this.exercise) { 42 | this.router.navigate(['/builder/exercises']); 43 | } else { 44 | this.exerciseBuilderService.buildingExercise = this.exercise; 45 | } 46 | }, 47 | (err:any) => { 48 | if (err.status === 404) { 49 | this.router.navigate(['/builder/exercises']) 50 | } else { 51 | console.error(err) 52 | } 53 | } 54 | ); 55 | } 56 | }); 57 | } 58 | 59 | ngDoCheck():any { 60 | if (!this.dataLoaded) { 61 | this.buildExerciseForm(); 62 | } 63 | } 64 | 65 | buildExerciseForm() { 66 | if (this.exercise) { 67 | this.dataLoaded = true; 68 | this.exerciseForm = this.formBuilder.group({ 69 | 'name': [this.exercise.name, [Validators.required, AlphaNumericValidator.invalidAlphaNumeric]], 70 | 'title': [this.exercise.title, Validators.required], 71 | 'description': [this.exercise.description, Validators.required], 72 | 'image': [this.exercise.image, Validators.required], 73 | 'nameSound': [this.exercise.nameSound], 74 | 'procedure': [this.exercise.procedure], 75 | 'videos': this.addVideoArray() 76 | }) 77 | } 78 | } 79 | 80 | addVideoArray():FormArray{ 81 | if(this.exercise.videos){ 82 | this.exercise.videos.forEach((video : any) => { 83 | this.videoArray.push(new FormControl(video, Validators.required)); 84 | }); 85 | } 86 | return this.videoArray; 87 | } 88 | 89 | onSubmit(formExercise:FormGroup){ 90 | this.submitted = true; 91 | if (!formExercise.valid) return; 92 | this.mapFormValues(formExercise); 93 | this.exerciseBuilderService.save(); 94 | this.router.navigate(['/builder/exercises']); 95 | } 96 | 97 | delete() { 98 | this.exerciseBuilderService.delete(); 99 | this.router.navigate( ['/builder/exercises'] ); 100 | } 101 | 102 | addVideo(){ 103 | this.exerciseBuilderService.addVideo(); 104 | let vidArray = this.exerciseForm.controls['videos']; 105 | vidArray.push(new FormControl("", Validators.required)); 106 | } 107 | 108 | canDeleteExercise(){ 109 | this.exerciseBuilderService.canDeleteExercise(); 110 | } 111 | 112 | deleteVideo(index: number){ 113 | this.exerciseBuilderService.deleteVideo(index); 114 | let vidArray = this.exerciseForm.controls['videos']; 115 | vidArray.removeAt(index); 116 | } 117 | 118 | mapFormValues(form: FormGroup){ 119 | this.exercise.name = form.controls['name'].value; 120 | this.exercise.title = form.controls['title'].value; 121 | this.exercise.description = form.controls['description'].value; 122 | this.exercise.image = form.controls['image'].value; 123 | this.exercise.nameSound = form.controls['nameSound'].value; 124 | this.exercise.procedure = form.controls['procedure'].value; 125 | this.exercise.videos = form.controls['videos'].value; 126 | } 127 | 128 | ngOnDestroy() { 129 | this.sub.unsubscribe(); 130 | } 131 | } -------------------------------------------------------------------------------- /trainer/src/services/workout-service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, fakeAsync, async, TestBed } from '@angular/core/testing'; 2 | import {HttpModule, Http, XHRBackend, Response, ResponseOptions} from '@angular/http'; 3 | import {MockBackend, MockConnection} from '@angular/http/testing'; 4 | 5 | import { Observable } from 'rxjs/Observable'; 6 | import 'rxjs/add/observable/of'; 7 | import 'rxjs/add/operator/catch'; 8 | import 'rxjs/add/operator/do'; 9 | import 'rxjs/add/operator/toPromise'; 10 | import 'rxjs/add/operator/map'; 11 | import 'rxjs/add/observable/forkJoin'; 12 | 13 | import {WorkoutService} from './workout-service'; 14 | import {WorkoutPlan} from "./model"; 15 | 16 | const makeWorkoutData = () => [ 17 | { name: "Workout1", title: "workout1" }, 18 | { name: "Workout2", title: "workout2" }, 19 | { name: "Workout3", title: "workout3" }, 20 | { name: "Workout4", title: "workout4" } 21 | ] as WorkoutPlan[]; 22 | 23 | describe('Workout Service', () => { 24 | let collectionUrl:string = 'https://api.mongolab.com/api/1/databases/personaltrainer/collections'; 25 | let apiKey:string = '9xfTWt1ilKhqIqzV9Z_8jvCzo5ksjexx'; 26 | let params:string = '?apiKey=' + apiKey; 27 | let workoutService:WorkoutService; 28 | let mockBackend:MockBackend; 29 | let fixture:any; 30 | 31 | beforeEach( async(() => { 32 | TestBed.configureTestingModule({ 33 | imports: [ HttpModule ], 34 | providers: [ 35 | WorkoutService, 36 | { provide: XHRBackend, useClass: MockBackend } 37 | ] 38 | }) 39 | })); 40 | 41 | it('can instantiate service when inject service', 42 | inject([WorkoutService], (service: WorkoutService) => { 43 | expect(service instanceof WorkoutService).toBe(true); 44 | })); 45 | 46 | it('can instantiate service with "new"', inject([Http], (http: Http) => { 47 | expect(http).not.toBeNull('http should be provided'); 48 | let service = new WorkoutService(http); 49 | expect(service instanceof WorkoutService).toBe(true, 'new service should be ok'); 50 | })); 51 | 52 | 53 | it('can provide the mockBackend as XHRBackend', 54 | inject([XHRBackend], (backend: MockBackend) => { 55 | expect(backend).not.toBeNull('backend should be provided'); 56 | })); 57 | 58 | it("should return all workout plans", fakeAsync((inject([XHRBackend, WorkoutService], (backend: MockBackend, service:WorkoutService) => { 59 | let result:any; 60 | backend.connections.subscribe((connection:MockConnection) => { 61 | expect(connection.request.url).toBe(collectionUrl + "/workouts" + params); 62 | let response = new ResponseOptions({body: '[{ "name": "Workout1", "title": "workout1" }, { "name": "Workout1", "title": "workout1" }]'}); 63 | connection.mockRespond(new Response(response)); 64 | }); 65 | service.getWorkouts().subscribe((response:Response) => { 66 | result = response; 67 | }); 68 | expect(result.length).toBe(2); 69 | expect(result[0] instanceof WorkoutPlan).toBe(true); 70 | })))); 71 | 72 | it("should return a workout plan with a specific name", fakeAsync((inject([XHRBackend, WorkoutService], (backend: MockBackend, service:WorkoutService) => { 73 | let result:any; 74 | backend.connections.subscribe((connection:MockConnection) => { 75 | if (connection.request.url === collectionUrl + "/workouts/Workout1" + params) { 76 | let response = new ResponseOptions({ 77 | body: '{ "name" : "Workout1" , "title" : "Workout 1" , "exercises" : [ { "name" : "exercise1" , "duration" : 30}]}' 78 | }); 79 | connection.mockRespond(new Response(response)); 80 | } else { 81 | connection.mockRespond(new Response( 82 | new ResponseOptions({ 83 | body: [{name: "exercise1", title: "exercise 1"}] 84 | }))); 85 | } 86 | }); 87 | service.getWorkout("Workout1").subscribe((response:Response) => { 88 | result = response; 89 | }); 90 | expect(result.name).toBe('Workout1'); 91 | })))); 92 | 93 | it("should map exercises to workout plan correctly in getWorkout", fakeAsync((inject([XHRBackend, WorkoutService], (backend: MockBackend, service:WorkoutService) => { 94 | let result:any; 95 | backend.connections.subscribe((connection:MockConnection) => { 96 | if (connection.request.url === collectionUrl + "/workouts/Workout1" + params) { 97 | let response = new ResponseOptions({ 98 | body: { name: "Workout1", title: "Workout 1", restBetweenExercise: 30, exercises: [{ name: "exercise2", duration: 31 }, { name: "exercise4", duration: 31 }] } 99 | }); 100 | connection.mockRespond(new Response(response)); 101 | } else { 102 | connection.mockRespond(new Response( 103 | new ResponseOptions({ 104 | body: [{ name: "exercise1", title: "exercise 1" }, { name: "exercise2", title: "exercise 2" }, { name: "exercise3", title: "exercise 3" }, { name: "exercise4", title: "exercise 4" }] 105 | }))); 106 | } 107 | }); 108 | service.getWorkout("Workout1").subscribe((response:Response) => { 109 | result = response; 110 | }); 111 | expect(result.name).toBe('Workout1'); 112 | expect(result.exercises.length).toBe(2); 113 | expect(result.exercises[0].name).toBe("exercise2"); 114 | expect(result.exercises[1].name).toBe("exercise4"); 115 | })))); 116 | }); 117 | 118 | -------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-runner.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild, EventEmitter, Output, Input, OnInit, DoCheck, OnDestroy} from '@angular/core'; 2 | import {Router} from '@angular/router'; 3 | 4 | import {WorkoutPlan, ExercisePlan, Exercise, ExerciseProgressEvent, ExerciseChangedEvent} from '../../services/model'; 5 | import {WorkoutHistoryTracker} from '../../services/workout-history-tracker'; 6 | import {WorkoutService} from "../../services/workout-service"; 7 | 8 | @Component({ 9 | selector: 'workout-runner', 10 | templateUrl: '/src/components/workout-runner/workout-runner.html' 11 | }) 12 | export class WorkoutRunnerComponent implements OnInit, DoCheck, OnDestroy { 13 | workoutPlan:WorkoutPlan; 14 | workoutTimeRemaining:number; 15 | restExercise:ExercisePlan; 16 | currentExerciseIndex:number; 17 | currentExercise:ExercisePlan; 18 | exerciseRunningDuration:number; 19 | exerciseTrackingInterval:number; 20 | workoutPaused:boolean; 21 | dataLoaded:boolean = false; 22 | @Input() workoutName:string; 23 | @Output() exercisePaused:EventEmitter = new EventEmitter(); 24 | @Output() exerciseResumed:EventEmitter = new EventEmitter(); 25 | @Output() exerciseProgress:EventEmitter = new EventEmitter(); 26 | @Output() exerciseChanged:EventEmitter = new EventEmitter(); 27 | @Output() workoutStarted:EventEmitter = new EventEmitter(); 28 | @Output() workoutComplete:EventEmitter = new EventEmitter(); 29 | 30 | constructor(private router:Router, 31 | private tracker:WorkoutHistoryTracker, 32 | private workoutService:WorkoutService) { 33 | } 34 | 35 | ngOnInit() { 36 | this.getWorkout(this.workoutName); 37 | } 38 | 39 | ngDoCheck():any { 40 | if (!this.dataLoaded) { 41 | this.start(); 42 | } 43 | } 44 | 45 | getWorkout(name:string) { 46 | this.workoutService.getWorkout(name) 47 | .subscribe( 48 | (data:WorkoutPlan) => { 49 | this.workoutPlan = data; 50 | }, 51 | (err:any) => { 52 | console.error(err) 53 | } 54 | ); 55 | } 56 | 57 | start() { 58 | if(this.workoutPlan) 59 | { 60 | this.dataLoaded = true; 61 | this.restExercise = new ExercisePlan(new Exercise("rest", "Relax!", "Relax a bit", "rest.png"), this.workoutPlan.restBetweenExercise); 62 | this.tracker.startTracking(); 63 | this.workoutTimeRemaining = this.workoutPlan.totalWorkoutDuration(); 64 | this.currentExerciseIndex = 0; 65 | this.startExercise(this.workoutPlan.exercises[this.currentExerciseIndex]); 66 | this.workoutStarted.emit(this.workoutPlan); 67 | } 68 | } 69 | 70 | pause() { 71 | clearInterval(this.exerciseTrackingInterval); 72 | this.workoutPaused = true; 73 | this.exercisePaused.emit(this.currentExerciseIndex); 74 | } 75 | 76 | resume() { 77 | this.startExerciseTimeTracking(); 78 | this.workoutPaused = false; 79 | this.exerciseResumed.emit(this.currentExerciseIndex); 80 | } 81 | 82 | pauseResumeToggle() { 83 | if (this.workoutPaused) { 84 | this.resume(); 85 | } 86 | else { 87 | this.pause(); 88 | } 89 | } 90 | 91 | onKeyPressed = function (event:KeyboardEvent) { 92 | if (event.which == 80 || event.which == 112) { // 'p' or 'P' key to toggle pause and resume. 93 | this.pauseResumeToggle(); 94 | } 95 | } 96 | 97 | startExercise(exercisePlan:ExercisePlan) { 98 | this.currentExercise = exercisePlan; 99 | this.exerciseRunningDuration = 0; 100 | this.startExerciseTimeTracking(); 101 | } 102 | 103 | getNextExercise():ExercisePlan { 104 | let nextExercise:ExercisePlan = null; 105 | if (this.currentExercise === this.restExercise) { 106 | nextExercise = this.workoutPlan.exercises[this.currentExerciseIndex + 1]; 107 | } 108 | else if (this.currentExerciseIndex < this.workoutPlan.exercises.length - 1) { 109 | nextExercise = this.restExercise; 110 | } 111 | return nextExercise; 112 | } 113 | 114 | startExerciseTimeTracking() { 115 | this.exerciseTrackingInterval = window.setInterval(() => { 116 | if (this.exerciseRunningDuration >= this.currentExercise.duration) { 117 | clearInterval(this.exerciseTrackingInterval); 118 | if (this.currentExercise !== this.restExercise) { 119 | this.tracker.exerciseComplete(this.workoutPlan.exercises[this.currentExerciseIndex]); 120 | } 121 | let next:ExercisePlan = this.getNextExercise(); 122 | if (next) { 123 | if (next !== this.restExercise) { 124 | this.currentExerciseIndex++; 125 | } 126 | this.startExercise(next); 127 | this.exerciseChanged.emit(new ExerciseChangedEvent(next, this.getNextExercise())); 128 | } 129 | else { 130 | this.tracker.endTracking(true); 131 | this.workoutComplete.emit(this.workoutPlan); 132 | this.router.navigate(['/finish', this.workoutName]); 133 | } 134 | return; 135 | } 136 | ++this.exerciseRunningDuration; 137 | --this.workoutTimeRemaining; 138 | this.exerciseProgress.emit(new ExerciseProgressEvent( 139 | this.currentExercise, 140 | this.exerciseRunningDuration, 141 | this.currentExercise.duration - this.exerciseRunningDuration, 142 | this.workoutTimeRemaining 143 | )); 144 | }, 1000); 145 | } 146 | 147 | ngOnDestroy() { 148 | this.tracker.endTracking(false); 149 | if (this.exerciseTrackingInterval) clearInterval(this.exerciseTrackingInterval); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /trainer/src/services/workout-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import {Http, Response, Headers, RequestOptions} from '@angular/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import 'rxjs/add/operator/map'; 5 | import 'rxjs/add/operator/catch'; 6 | import 'rxjs/add/observable/forkJoin'; 7 | import 'rxjs/add/operator/toPromise'; 8 | 9 | import { Exercise, ExercisePlan, WorkoutPlan } from './model'; 10 | 11 | @Injectable() 12 | export class WorkoutService { 13 | workouts: Array = []; 14 | exercises: Array = []; 15 | workout: WorkoutPlan; 16 | collectionsUrl = 'https://api.mongolab.com/api/1/databases/personaltrainer/collections'; 17 | apiKey = '9xfTWt1ilKhqIqzV9Z_8jvCzo5ksjexx'; 18 | params = '?apiKey=' + this.apiKey; 19 | 20 | constructor(public http: Http) { 21 | } 22 | 23 | getExercises(){ 24 | return this.http.get(this.collectionsUrl + '/exercises' + this.params) 25 | .map((res: Response) => res.json()) 26 | .catch(WorkoutService.handleError); 27 | } 28 | 29 | getExercise(exerciseName: string){ 30 | return this.http.get(this.collectionsUrl + '/exercises/' + exerciseName + this.params) 31 | .map((res: Response) => res.json()) 32 | .catch(WorkoutService.handleError); 33 | } 34 | 35 | updateExercise(exercise: Exercise){ 36 | for (var i = 0; i < this.exercises.length; i++) { 37 | if (this.exercises[i].name === exercise.name) { 38 | this.exercises[i] = exercise; 39 | } 40 | } 41 | return exercise; 42 | } 43 | 44 | addExercise(exercise: Exercise){ 45 | if (exercise.name) { 46 | this.exercises.push(exercise); 47 | return exercise; 48 | } 49 | } 50 | 51 | deleteExercise(exerciseName: string){ 52 | let exerciseIndex: number; 53 | for (var i = 0; i < this.exercises.length; i++) { 54 | if (this.exercises[i].name === exerciseName) { 55 | exerciseIndex = i; 56 | } 57 | } 58 | if (exerciseIndex >= 0) this.exercises.splice(exerciseIndex, 1); 59 | } 60 | 61 | getWorkouts(){ 62 | return this.http.get(this.collectionsUrl + '/workouts' + this.params) 63 | .map((res:Response) => res.json()) 64 | .map((workouts:Array) => { 65 | let result:Array = []; 66 | if (workouts) { 67 | workouts.forEach((workout) => { 68 | result.push( 69 | new WorkoutPlan( 70 | workout.name, 71 | workout.title, 72 | workout.restBetweenExercise, 73 | workout.exercises, 74 | workout.description 75 | )); 76 | }); 77 | } 78 | return result; 79 | }) 80 | .catch(WorkoutService.handleError); 81 | } 82 | 83 | getWorkout(workoutName:string) { 84 | return Observable.forkJoin( 85 | this.http.get(this.collectionsUrl + '/exercises' + this.params).map((res:Response) => res.json()), 86 | this.http.get(this.collectionsUrl + '/workouts/' + workoutName + this.params).map((res:Response) => res.json()) 87 | ).map( 88 | (data:any) => { 89 | let allExercises = data[0]; 90 | let workout = new WorkoutPlan( 91 | data[1].name, 92 | data[1].title, 93 | data[1].restBetweenExercise, 94 | data[1].exercises, 95 | data[1].description 96 | ) 97 | workout.exercises.forEach( 98 | (exercisePlan:any) => exercisePlan.exercise = allExercises.find( 99 | (x:any) => x.name === exercisePlan.name 100 | ) 101 | ) 102 | return workout; 103 | } 104 | ) 105 | .catch(WorkoutService.handleError); 106 | } 107 | 108 | addWorkout(workout:any) { 109 | let workoutExercises:any = []; 110 | workout.exercises.forEach( 111 | (exercisePlan:any) => { 112 | workoutExercises.push({name: exercisePlan.exercise.name, duration: exercisePlan.duration}) 113 | } 114 | ); 115 | 116 | let body = { 117 | "_id": workout.name, 118 | "exercises": workoutExercises, 119 | "name": workout.name, 120 | "title": workout.title, 121 | "description": workout.description, 122 | "restBetweenExercise": workout.restBetweenExercise 123 | }; 124 | 125 | return this.http.post(this.collectionsUrl + '/workouts' + this.params, body) 126 | .map((res:Response) => res.json()) 127 | .catch(WorkoutService.handleError) 128 | } 129 | 130 | updateWorkout(workout:WorkoutPlan) { 131 | let workoutExercises:any = []; 132 | workout.exercises.forEach( 133 | (exercisePlan:any) => { 134 | workoutExercises.push({name: exercisePlan.exercise.name, duration: exercisePlan.duration}) 135 | } 136 | ); 137 | 138 | let body = { 139 | "_id": workout.name, 140 | "exercises": workoutExercises, 141 | "name": workout.name, 142 | "title": workout.title, 143 | "description": workout.description, 144 | "restBetweenExercise": workout.restBetweenExercise 145 | }; 146 | 147 | return this.http.put(this.collectionsUrl + '/workouts/' + workout.name + this.params, body) 148 | .map((res:Response) => res.json()) 149 | .catch(WorkoutService.handleError); 150 | } 151 | 152 | deleteWorkout(workoutName:string) { 153 | return this.http.delete(this.collectionsUrl + '/workouts/' + workoutName + this.params) 154 | .map((res:Response) => res.json()) 155 | .catch(WorkoutService.handleError) 156 | } 157 | 158 | static handleError(error: Response) { 159 | console.log(error); 160 | return Observable.throw(error || 'Server error'); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /trainer/src/components/workout-builder/workout/workout.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

{{workout.title||'Workout Title'}}

7 |
8 |
9 |
10 |
11 |
12 |
13 | {{exercisePlan.exercise.title}} 14 |
15 |
16 |
{{i +1}}
17 |
18 |
19 | 20 |
21 |
22 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 | 45 |
46 | 47 | 48 |
49 |
50 |
51 | 52 | 53 |
54 | 55 | 56 |
57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 |

{{workout.exercises?.length}}

69 |
70 |
71 | 72 |

{{workout.totalWorkoutDuration()|secondsToTime}}

73 |

74 |
75 |
76 | 77 | 78 | Save 79 | 80 |
81 |
82 |
83 |
84 |
85 |
-------------------------------------------------------------------------------- /trainer/src/components/workout-runner/workout-runner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, fakeAsync, async, tick, TestBed } from '@angular/core/testing'; 2 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 3 | import {Router} from '@angular/router'; 4 | import {Observable} from "rxjs/Rx"; 5 | 6 | import {WorkoutHistoryTracker} from '../../services/workout-history-tracker'; 7 | import {WorkoutRunnerComponent} from './workout-runner.component'; 8 | import {WorkoutService} from '../../services/workout-service'; 9 | import {Exercise, WorkoutPlan, ExercisePlan} from "../../services/model"; 10 | import {SecondsToTimePipe} from "../shared/seconds-to-time.pipe"; 11 | 12 | class MockWorkoutHistoryTracker { 13 | startTracking() {} 14 | endTracking() {} 15 | exerciseComplete() {} 16 | } 17 | 18 | class MockWorkoutService { 19 | 20 | sampleWorkout = new WorkoutPlan( 21 | "testworkout", 22 | "Test Workout", 23 | 40, 24 | [ 25 | new ExercisePlan(new Exercise( "exercise1", "Exercise 1", "Exercise 1 description", "/image1/path", "audio1/path"), 50), 26 | new ExercisePlan(new Exercise( "exercise1", "Exercise 2", "Exercise 2 description", "/image2/path", "audio2/path"), 30), 27 | new ExercisePlan(new Exercise( "exercise1", "Exercise 3", "Exercise 3 description", "/image3/path", "audio3/path"), 20) 28 | ], 29 | "This is a test workout" 30 | ); 31 | 32 | getWorkout(name: string) { 33 | return Observable.of(this.sampleWorkout); 34 | } 35 | totalWorkoutDuration(){ 36 | return 180; 37 | }; 38 | } 39 | 40 | export class MockRouter { 41 | navigate = jasmine.createSpy('navigate'); 42 | } 43 | 44 | describe('Workout Runner', () =>{ 45 | let fixture:any; 46 | let runner:any; 47 | 48 | beforeEach( async(() =>{ 49 | TestBed 50 | .configureTestingModule({ 51 | declarations: [ WorkoutRunnerComponent, SecondsToTimePipe ], 52 | providers: [ 53 | {provide: Router, useClass: MockRouter}, 54 | {provide: WorkoutHistoryTracker ,useClass: MockWorkoutHistoryTracker}, 55 | {provide: WorkoutService ,useClass: MockWorkoutService} 56 | ], 57 | schemas: [ NO_ERRORS_SCHEMA ] 58 | }) 59 | .compileComponents() 60 | .then(() => { 61 | fixture = TestBed.createComponent(WorkoutRunnerComponent); 62 | runner = fixture.componentInstance; 63 | }); 64 | })); 65 | 66 | it('should instantiate the Workout Runner Component', () => { 67 | expect(fixture.componentInstance instanceof WorkoutRunnerComponent).toBe(true, 'should create WorkoutRunnerComponent'); 68 | }); 69 | 70 | it('should start the workout', () => { 71 | runner.workoutStarted.subscribe((w: any) => { 72 | expect(w).toEqual(runner.workoutPlan); 73 | }); 74 | runner.ngOnInit(); 75 | runner.ngDoCheck(); 76 | expect(runner.workoutTimeRemaining).toEqual(runner.workoutPlan.totalWorkoutDuration()); 77 | expect(runner.workoutPaused).toBeFalsy(); 78 | }); 79 | 80 | it('should start the first exercise', () => { 81 | spyOn(runner, 'startExercise').and.callThrough(); 82 | runner.ngOnInit(); 83 | runner.ngDoCheck(); 84 | expect(runner.currentExerciseIndex).toEqual(0); 85 | expect(runner.startExercise).toHaveBeenCalledWith(runner.workoutPlan.exercises[runner.currentExerciseIndex]); 86 | expect(runner.currentExercise).toEqual(runner.workoutPlan.exercises[0]); 87 | }); 88 | 89 | it("should start history tracking", inject([WorkoutHistoryTracker], (tracker: WorkoutHistoryTracker) => { 90 | spyOn(tracker, 'startTracking'); 91 | runner.ngOnInit(); 92 | runner.ngDoCheck(); 93 | expect(tracker.startTracking).toHaveBeenCalled(); 94 | })); 95 | 96 | it('should increase current exercise duration with time', fakeAsync(() => { 97 | runner.ngOnInit(); 98 | runner.ngDoCheck(); 99 | expect(runner.exerciseRunningDuration).toBe(0); 100 | tick(1000); 101 | expect(runner.exerciseRunningDuration).toBe(1); 102 | tick(1000); 103 | expect(runner.exerciseRunningDuration).toBe(2); 104 | TestHelper.advanceWorkout(7); 105 | expect(runner.exerciseRunningDuration).toBe(10); 106 | runner.ngOnDestroy(); 107 | })); 108 | 109 | it("should decrease total workout duration with time", fakeAsync(() => { 110 | runner.ngOnInit(); 111 | runner.ngDoCheck(); 112 | expect(runner.workoutTimeRemaining).toBe(runner.workoutPlan.totalWorkoutDuration()); 113 | tick(1000); 114 | expect(runner.workoutTimeRemaining).toBe(runner.workoutPlan.totalWorkoutDuration() - 1); 115 | tick(1000); 116 | expect(runner.workoutTimeRemaining).toBe(runner.workoutPlan.totalWorkoutDuration() - 2); 117 | runner.ngOnDestroy(); 118 | })); 119 | 120 | it("should transition to next exercise on one exercise complete", fakeAsync(() => { 121 | runner.ngOnInit(); 122 | runner.ngDoCheck(); 123 | let exerciseDuration = runner.workoutPlan.exercises[0].duration; 124 | TestHelper.advanceWorkout(exerciseDuration); 125 | expect(runner.currentExercise.exercise.name).toBe('rest'); 126 | expect(runner.currentExercise.duration).toBe(runner.workoutPlan.restBetweenExercise); 127 | runner.ngOnDestroy(); 128 | })); 129 | 130 | it("should not update workoutTimeRemaining for paused workout on interval lapse", fakeAsync(() => { 131 | runner.ngOnInit(); 132 | runner.ngDoCheck(); 133 | expect(runner.workoutPaused).toBeFalsy(); 134 | tick(1000); 135 | expect(runner.workoutTimeRemaining).toBe(runner.workoutPlan.totalWorkoutDuration() - 1); 136 | runner.pause(); 137 | expect(runner.workoutPaused).toBe(true); 138 | tick(1000); 139 | expect(runner.workoutTimeRemaining).toBe(runner.workoutPlan.totalWorkoutDuration() - 1); 140 | runner.ngOnDestroy(); 141 | })); 142 | 143 | it("should end the workout when all exercises are complete", inject([WorkoutHistoryTracker, Router],fakeAsync(( tracker: WorkoutHistoryTracker, router:Router) => { 144 | spyOn(tracker, 'endTracking'); 145 | runner.ngOnInit(); 146 | runner.ngDoCheck(); 147 | runner.workoutName = runner.workoutPlan.name; 148 | TestHelper.advanceWorkout(runner.workoutPlan.exercises[0].duration); 149 | TestHelper.advanceWorkout(runner.workoutPlan.restBetweenExercise); 150 | TestHelper.advanceWorkout(runner.workoutPlan.exercises[1].duration); 151 | TestHelper.advanceWorkout(runner.workoutPlan.restBetweenExercise); 152 | TestHelper.advanceWorkout(runner.workoutPlan.exercises[2].duration); 153 | 154 | expect(tracker.endTracking).toHaveBeenCalled(); 155 | expect(router.navigate).toHaveBeenCalledWith(['/finish', runner.workoutPlan.name]); 156 | expect(runner.workoutTimeRemaining).toBe(0); 157 | expect(runner.currentExercise).toBe(runner.workoutPlan.exercises[2]); 158 | runner.ngOnDestroy(); 159 | }))); 160 | }); 161 | 162 | class TestHelper { 163 | static advanceWorkout(duration: number){ 164 | for (var i= 0; i <= duration; i++){tick(1000) 165 | }; 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /trainer/db/seed.js: -------------------------------------------------------------------------------- 1 | /* Workout list 2 | To import workout list use a tool that can make POST request. The below instruction are using POSTMAN addin for chrome browser. Other tools like CURL, that can make http requests can also be used instead of POSTMAN. 3 | 1. Open POSTMAN and paste the url https://api.mongolab.com/api/1/databases//collections/workouts?apiKey= 4 | 2. Update url with your database name () and api key (). 5 | 3. Change option from action dropdown to POST. 6 | 4. Change data format tab to "raw" 7 | 5. Click on the "Headers" button or the top right next to "URL Params" button. 8 | 6. Clicking on "Headers" shows up a key-value data entry section. Add key "Content-Type" and value "application/json". 9 | 7. Copy and paste the below json array in the text area. 10 | 8. Click the button "Send". 11 | 9. Check for sucess response 12 | */ 13 | [{ "_id": "7minworkout", "exercises": [{ "name": "jumpingJacks", "duration": 30 }, { "name": "wallSit", "duration": 30 }, { "name": "pushUp", "duration": 30 }, { "name": "crunches", "duration": 30 }, { "name": "stepUpOntoChair", "duration": 30 }, { "name": "squat", "duration": 30 }, { "name": "tricepdips", "duration": 30 }, { "name": "plank", "duration": 30 }, { "name": "highKnees", "duration": 30 }, { "name": "lunges", "duration": 30 }, { "name": "pushupNRotate", "duration": 30 }, { "name": "sidePlank", "duration": 30 }], "name": "7minworkout", "title": "7 Minute Workout", "description": "A high intensity workout that consists of 12 exercises.", "restBetweenExercise": 10 }] 14 | 15 | /* Exercise list 16 | To import exercise list use a tool that can make POST request. The below instruction are using POSTMAN addin for chrome browser. Other tools like CURL, that can make http requests can also be used instead of POSTMAN. 17 | 1. Open POSTMAN and paste the url https://api.mongolab.com/api/1/databases//collections/exercises?apiKey= 18 | 2. Update url with your database name () and api key (). 19 | 3. Follow step 3 and 9 from above. 20 | */ 21 | [{ "_id": "jumpingJacks", "name": "jumpingJacks", "title": "Jumping Jacks", "description": "A jumping jack or star jump, also called side-straddle hop is a physical jumping exercise.", "image": "JumpingJacks.png", "nameSound": "content/jumpingjacks.wav", "videos": ["dmYwZH_BNd0", "BABOdJ-2Z6o", "c4DAnQ6DtF8"] , "procedure": "Assume an erect position, with feet together and arms at your side.
Slightly bend your knees, and propel yourself a few inches into the air.
While in air, bring your legs out to the side about shoulder width or slightly wider.
As you are moving your legs outward, you should raise your arms up over your head; arms should be slightly bent throughout the entire in-air movement.
Your feet should land shoulder width or wider as your hands meet above your head with arms slightly bent" }, { "_id": "wallSit", "name": "wallSit", "title": "Wall Sit", "description": "A wall sit, also known as a Roman Chair, is an exercise done to strengthen the quadriceps muscles.", "image": "wallsit.png", "nameSound": "content/wallsit.wav", "videos": ["y-wV4Venusw", "MMV3v4ap4ro"] , "procedure": "Place your back against a wall with your feet shoulder width apart and a little ways out from the wall.
Then, keeping your back against the wall, lower your hips until your knees form right angles. " }, { "_id": "crunches", "name": "crunches", "title": "Abdominal Crunches", "description": "The basic crunch is a abdominal exercise in a strength-training program.", "image": "crunches.png", "nameSound": "content/crunches.wav", "videos": ["Xyd_fa5zoEU", "MKmrqcoCZ-M"], "procedure": "Lie on your back with your knees bent and feet flat on the floor, hip-width apart. Place your hands behind your head so your thumbs are behind your ears. Hold your elbows out to the sides but rounded slightly in. Gently pull your abdominals inward. Curl up and forward so that your head, neck, and shoulder blades lift off the floor. Hold for a moment at the top of the movement and then lower slowly back down." }, { "_id": "stepUpOntoChair", "name": "stepUpOntoChair", "title": "Step Up Onto Chair", "description": "Step exercises are ideal for building muscle in your lower body.", "image": "stepUpOntoChair.png", "nameSound": "content/stepup.wav", "videos": ["aajhW7DD1EA"], "procedure": "Position your chair in front of you.Stand with your feet about hip width apart, arms at your sides. Step up onto the seat with one foot, pressing down while bringing your other foot up next to it. Step back with the leading foot and bring the trailing foot down to finish one step-up." }, { "_id": "tricepdips", "name": "tricepdips", "title": "Tricep Dips On Chair", "description": "A body weight exercise that targets triceps.", "image": "tricepdips.png", "nameSound": "content/tricepdips.wav", "videos": ["tKjcgfu44sI", "jox1rb5krQI"], "procedure": "Sit up on a chair. Your legs should be slightly extended, with your feet flat on the floor.Place your hands edges of the chair. Your palms should be down, fingertips pointing towards the floor.\\\n Without moving your legs, bring your glutes forward off the chair.Steadily lower yourself. When your elbows form 90 degrees angles, push yourself back up to starting position." }, { "_id": "plank", "name": "plank", "title": "Plank", "description": "The plank (also called a front hold, hover, or abdominal bridge) is an isometric core strength exercise that involves maintaining a difficult position for extended periods of time. ", "image": "plank.png", "nameSound": "content/plank.wav", "videos": ["pSHjTRCQxIw", "TvxNkmjdhMM"], "procedure": "Get into pushup position on the floor. Bend your elbows 90 degrees and rest your weight on your forearms. Your elbows should be directly beneath your shoulders, and your body should form a straight line from head to feet. Hold this position." }, { "_id": "highKnees", "name": "highKnees", "title": "High Knees", "description": "A form exercise that develops strength and endurance of the hip flexors and quads and stretches the hip extensors.", "image": "highknees.png", "nameSound": "content/highknees.wav", "videos": ["OAJ_J3EZkdY", "8opcQdC-V-U"], "procedure": "Start standing with feet hip-width apart. Do inplace jog with your knees lifting as much as possible towards your chest." }, { "_id": "lunges", "name": "lunges", "title": "Lunges", "description": "Lunges are a good exercise for strengthening, sculpting and building several muscles/muscle groups, including the quadriceps (or thighs), the gluteus maximus (or buttocks) as well as the hamstrings. ", "image": "lunges.png", "nameSound": "content/lunge.wav", "videos": ["Z2n58m2i4jg"], "procedure": "Stand erect with your feet about one shoulder width apart.Put your hands on your hips, keep your back as straight as possible, relax your shoulders and keep your eyes facing directly ahead. Take a large step forward with one leg. As you step forward, lower your hips and bend your knees until they both form 90 degree angles. Return to starting position. Repeat with your alternate leg." }, { "_id": "pushupNRotate", "name": "pushupNRotate", "title": "Pushup And Rotate", "description": "A variation of pushup that requires you to rotate.", "image": "pushupNRotate.png", "nameSound": "content/pushupandrotate.wav", "videos": ["qHQ_E-f5278"], "procedure": "Assume the classic pushup position, but as you come up, rotate your body so your right arm lifts up and extends overhead.Return to the starting position, lower yourself, then push up and rotate till your left hand points toward the ceiling." }, { "_id": "sidePlank", "name": "sidePlank", "title": "Side Plank", "description": "A variation to Plank done using one hand only", "image": "sideplank.png", "nameSound": "content/sideplank.wav", "videos": ["wqzrb67Dwf8", "_rdfjFSFKMY"], "procedure": "Lie on your side, in a straight line from head to feet, resting on your forearm.Your elbow should be directly under your shoulder.With your abdominals gently contracted, lift your hips off the floor, maintaining the line. Keep your hips square and your neck in line with your spine. Hold the position." }, { "_id": "pushUp", "name": "pushUp", "title": "Push Up", "description": "A push-up is a common exercise performed in a prone position by raising and lowering the body using the arms", "image": "pushup.png", "nameSound": "content/pushups.wav", "videos": ["Eh00_rniF8E", "ZWdBqFLNljc", "UwRLWMcOdwI", "ynPwl6qyUNM", "OicNTT2xzMI"], "procedure": "Lie prone on the ground with hands placed as wide or slightly wider than shoulder width. Keeping the body straight, lower body to the ground by bending arms at the elbows. Raise body up off the ground by extending the arms." }, { "_id": "squat", "name": "squat", "title": "Squat", "description": "The squat is a compound, full body exercise that trains primarily the muscles of the thighs, hips, buttocks and quads.", "image": "squat.png", "nameSound": "content/squats.wav", "videos": ["QKKZ9AGYTi4", "UXJrBgI2RxA"], "procedure": "Stand with your head facing forward and your chest held up and out.Place your feet shoulder-width apart or little wider. Extend your hands straight out in front of you. Sit back and down like you're sitting into a chair. Keep your head facing straight as your upper body bends forward a bit. Rather than allowing your back to round, let your lower back arch slightly as you go down. Lower down so your thighs are parallel to the floor, with your knees over your ankles. Press your weight back into your heels. Keep your body tight, and push through your heels to bring yourself back to the starting position." }] 22 | 23 | -------------------------------------------------------------------------------- /trainer/static/css/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | html{ 4 | height:100%; 5 | } 6 | 7 | body { 8 | padding: 108px 15px 20px 15px; 9 | min-height: 100%; 10 | font-size: 13px; 11 | font-family: 'Roboto Slab'; 12 | font-style: normal; 13 | font-weight: 400; 14 | } 15 | 16 | /* Set width on the form input elements since they're 100% wide by default */ 17 | input, 18 | select, 19 | textarea { 20 | max-width: 280px; 21 | } 22 | 23 | /* styles for validation helpers - start*/ 24 | .field-validation-error { 25 | color: #b94a48; 26 | } 27 | 28 | .field-validation-valid { 29 | display: none; 30 | } 31 | 32 | input.input-validation-error { 33 | border: 1px solid #b94a48; 34 | } 35 | 36 | input[type="checkbox"].input-validation-error { 37 | border: 0 none; 38 | } 39 | 40 | .validation-summary-errors { 41 | color: #b94a48; 42 | } 43 | 44 | .validation-summary-valid { 45 | display: none; 46 | } 47 | 48 | .validation-message{ 49 | width:280px 50 | } 51 | 52 | .extended-validation-message{ 53 | width:320px 54 | } 55 | /* styles for validation helpers - end */ 56 | 57 | /*styles for top nav bar - start*/ 58 | .navbar-default.top-navbar { 59 | background-color: #4eaded; 60 | border-color: #4eaded; 61 | } 62 | .second-top-nav{ 63 | background-color: #0088dc !important; 64 | color: white; 65 | height:43px; 66 | position:fixed !important; 67 | top:70px !important; 68 | } 69 | .second-top-nav div{ 70 | position:fixed; 71 | top:81px; 72 | } 73 | .navbar-default .second-top-nav a { 74 | color: white !important; 75 | } 76 | 77 | .navbar-default.top-navbar h1 { 78 | color: white; 79 | margin-top: 15px; 80 | margin-bottom: 15px; 81 | } 82 | /*styles for top nav bar - end*/ 83 | 84 | /*styles for left nav bar - start*/ 85 | .left-nav-bar { 86 | background-color: #1ba2db; 87 | min-height: 100%; 88 | position: fixed; 89 | margin-top: -14px; 90 | margin-left:-15px; 91 | left:0px; 92 | width:325px; 93 | } 94 | 95 | .left-nav-bar div{ 96 | position:fixed; 97 | top:123px; 98 | padding-left:5px; 99 | font-size:14px; 100 | } 101 | 102 | .left-nav-bar h3{ 103 | padding-top:20px; 104 | top:123px; 105 | text-align:center 106 | } 107 | .left-nav-bar a{ 108 | background-color:#995466; 109 | color:white; 110 | text-align:center; 111 | width:300px; 112 | } 113 | 114 | .left-nav-bar a:hover{ 115 | background-color:#1D89CF; 116 | color:white; 117 | text-align:center; 118 | width:300px; 119 | } 120 | 121 | .left-nav-bar a:active{ 122 | background-color:#1D89CF; 123 | color:white; 124 | text-align:center; 125 | width:300px; 126 | } 127 | 128 | /*styles for left nav bar - end*/ 129 | 130 | /*styles for left nav exercises bar - start*/ 131 | .left-nav-exercises-bar { 132 | background-color: #1ba2db; 133 | min-height: 100%; 134 | max-height: 100%; 135 | position: fixed; 136 | margin-top: -14px; 137 | margin-left:-15px; 138 | left:0px; 139 | width:325px; 140 | overflow-y: auto; 141 | } 142 | 143 | .left-nav-exercises-bar div{ 144 | top:123px; 145 | padding-left:5px 146 | } 147 | 148 | .left-nav-exercises-bar button{ 149 | background-color:#995466; 150 | color:white; 151 | text-align:center; 152 | width:300px; 153 | } 154 | 155 | .left-nav-exercises-bar h3{ 156 | padding-top:20px; 157 | top:123px; 158 | text-align:center 159 | } 160 | .left-nav-exercises-bar a{ 161 | background-color:#995466; 162 | color:white; 163 | text-align:center; 164 | width:180px; 165 | } 166 | /*styles for left nav bar - end*/ 167 | 168 | /*styles for container - start*/ 169 | .container.app-container{ 170 | width:100%; 171 | max-width:100%; 172 | } 173 | .workout-display-div { 174 | text-align: center; 175 | padding: 40px; 176 | } 177 | .workout-display-div .img-responsive{ 178 | margin: 25px auto; 179 | width:540px; 180 | height:360px; 181 | cursor:pointer; 182 | } 183 | .workout-display-div .time-progress{ 184 | margin-top:40px; 185 | } 186 | /*styles for container - end*/ 187 | 188 | /*Styles for start page - start*/ 189 | .text-center{ 190 | color:#4eaded; 191 | } 192 | 193 | .action{ 194 | text-align:center; 195 | margin-top:25px; 196 | } 197 | .action a { 198 | text-decoration: none; 199 | font-size: 54px; 200 | color:#4eaded; 201 | } 202 | .action a:hover{ 203 | color:#005E9E; 204 | } 205 | .action a .glyphicon { 206 | top:8px; 207 | } 208 | /*Styles for start page - end*/ 209 | 210 | /*Styles for workout page - start*/ 211 | .workout-app-container{ 212 | padding-top:15px; 213 | } 214 | 215 | #video-panel .panel-body{ 216 | max-height:650px; 217 | overflow:auto; 218 | } 219 | #pause-overlay { 220 | position:absolute; 221 | top:0px; 222 | width:100%; 223 | height:100%; 224 | opacity:0; /*Comment this line to try our mouse event based pause and resume. */ 225 | z-index:10; 226 | 227 | } 228 | 229 | /*Comment this style to try our mouse event based pause and resume. */ 230 | #pause-overlay:hover { 231 | opacity:.8; 232 | } 233 | #pause-overlay .pause{ 234 | font-size:120pt; 235 | width:140px; 236 | height:140px; 237 | color:#4eaded; 238 | } 239 | .absolute-center { 240 | margin: auto; 241 | position: absolute; 242 | top: 0; left: 0; bottom: 0; right: 0; 243 | } 244 | 245 | #play-video-overlay { 246 | position:absolute; 247 | top:0px; 248 | width:100%; 249 | height:100%; 250 | opacity:0; 251 | z-index:10; 252 | 253 | } 254 | #play-video-overlay:hover { 255 | opacity:.6; 256 | } 257 | #play-video-overlay .video { 258 | font-size:80pt; 259 | width:100px; 260 | height:100px; 261 | color:#4eaded; 262 | } 263 | /*Styles for workout page - end*/ 264 | 265 | /*Animation effects workout*/ 266 | div[ng-view] { 267 | position: absolute; 268 | width: 100%; 269 | height: 100%; 270 | } 271 | div[ng-view].ng-enter, 272 | div[ng-view].ng-leave { 273 | -webkit-transition: all 1s ease; 274 | -moz-transition: all 1s ease; 275 | -o-transition: all 1s ease; 276 | transition: all 1s ease; 277 | } 278 | div[ng-view].ng-enter { 279 | left: 100%; /*initial css for view transition in*/ 280 | } 281 | div[ng-view].ng-leave { 282 | left: 0%; /*initial css for view transition out*/ 283 | } 284 | div[ng-view].ng-enter-active { 285 | left: 0%; /*final css for view transition in*/ 286 | } 287 | div[ng-view].ng-leave-active { 288 | left: -100%; /*final css for view transition out*/ 289 | } 290 | 291 | .video-image.ng-enter, 292 | .video-image.ng-move { 293 | -webkit-animation: bounceIn 1s; 294 | -moz-animation: bounceIn 1s; 295 | -ms-animation: bounceIn 1s; 296 | animation: bounceIn 1s; 297 | } 298 | .video-image.ng-leave { 299 | -webkit-animation: bounceOut 1s; 300 | -moz-animation: bounceOut 1s; 301 | -ms-animation: bounceOut 1s; 302 | animation: bounceOut 1s; 303 | } 304 | 305 | /*Animation effects Workout- end*/ 306 | 307 | /*Workout Builder Styles*/ 308 | #left-nav-exercises button { 309 | margin-bottom:4px; 310 | } 311 | #left-nav-exercises button .glyphicon { 312 | margin-left:6px; 313 | } 314 | .exercise-item.ng-enter, 315 | .exercise-item.ng-move { 316 | -webkit-animation: zoomIn 0.5s; 317 | -moz-animation: zoomIn 0.5s; 318 | -ms-animation: zoomIn 0.5s; 319 | animation: zoomIn 0.5s; 320 | } 321 | .exercise-item.ng-leave { 322 | -webkit-animation: fadeOut 0.25s; 323 | -moz-animation: fadeOut 0.25s; 324 | -ms-animation: fadeOut 0.25s; 325 | animation: fadeOut 0.25s; 326 | } 327 | /*Workout Builder Styles - end*/ 328 | 329 | 330 | /*styles for tile - start*/ 331 | .exercise.tile { 332 | width:320px; 333 | height:226px; 334 | color:white; 335 | margin-bottom:10px; 336 | background:#61b9ff; 337 | } 338 | 339 | .exercise.tile .title { 340 | width:inherit; 341 | background:#9d4a9c; 342 | font-size:14pt; 343 | padding:5px; 344 | } 345 | .exercise.tile .index{ 346 | width:40px; 347 | float:left; 348 | font-size:20pt; 349 | text-align:center; 350 | padding-top:10px; 351 | } 352 | .exercise.tile .order{ 353 | width:53px; 354 | float:right; 355 | } 356 | .exercise.tile .main { 357 | width:225px; 358 | float:left; 359 | padding:4px; 360 | } 361 | .exercise.tile img{ 362 | width:100%; 363 | height:120px; 364 | } 365 | 366 | .workouts-container{ 367 | position:fixed; 368 | left:350px; 369 | height:90%; 370 | overflow-y:auto; 371 | width:100%; 372 | } 373 | 374 | .workout-container{ 375 | position:fixed; 376 | left:350px; 377 | height:90%; 378 | overflow-y:auto; 379 | width:100%; 380 | } 381 | 382 | .workout-data{ 383 | padding-left: 30px; 384 | } 385 | 386 | .workout.tile { 387 | width: 300px; 388 | height: 80px; 389 | background: #5bc0de; 390 | color: white; 391 | padding:10px; 392 | margin-bottom:10px; 393 | } 394 | .workout.tile:hover{ 395 | background:#1D89CF; 396 | } 397 | 398 | .workout .title{ 399 | font-size: 20pt; 400 | text-align: center; 401 | } 402 | .workout .stats{ 403 | font-size: 12pt; 404 | padding:0 10px 0 10px; 405 | } 406 | 407 | .list-title{ 408 | max-width:300px; 409 | } 410 | 411 | .exercise-list{ 412 | min-width: 350px; 413 | } 414 | 415 | .exercise-tile { 416 | width: 300px; 417 | height: 50px; 418 | background: #5bc0de; 419 | color: white; 420 | padding:5px; 421 | margin-bottom:10px; 422 | font-size: 18pt; 423 | text-align: center; 424 | } 425 | .exercise-tile:hover { 426 | background:#1D89CF; 427 | } 428 | 429 | .exercise-container{ 430 | position:fixed; 431 | height:90%; 432 | overflow-y:auto; 433 | width:100%; 434 | } 435 | 436 | .exercise-data{ 437 | padding-left: 30px; 438 | max-width:320px; 439 | } 440 | 441 | .exercise-video{ 442 | padding-left: 30px; 443 | max-width:350px; 444 | } 445 | 446 | .img-preview{ 447 | width:250px; 448 | height:250px 449 | } 450 | 451 | .exercises-container{ 452 | position:fixed; 453 | left:350px; 454 | height:85%; 455 | overflow-y:auto; 456 | width:100%; 457 | } 458 | 459 | .exercises-leftnav-container{ 460 | position:fixed; 461 | } 462 | /*styles for tile- end*/ 463 | 464 | /*styles for spinner- start*/ 465 | .spin { 466 | -webkit-animation: spin .8s infinite linear; 467 | -moz-animation: spin 1s infinite linear; 468 | -o-animation: spin 1s infinite linear; 469 | animation: spin 1s infinite linear; 470 | } 471 | 472 | @-moz-keyframes spin { 473 | from { 474 | -moz-transform: rotate(0deg); 475 | } 476 | to { 477 | -moz-transform: rotate(360deg); 478 | } 479 | } 480 | 481 | @-webkit-keyframes spin { 482 | from { 483 | -webkit-transform: rotate(0deg); 484 | } 485 | to { 486 | -webkit-transform: rotate(360deg); 487 | } 488 | } 489 | 490 | @keyframes spin { 491 | from { 492 | transform: rotate(0deg); 493 | } 494 | to { 495 | transform: rotate(360deg); 496 | } 497 | } 498 | /*styles for spinner- end*/ 499 | 500 | 501 | .label-as-badge { 502 | border-radius: 1em; 503 | } 504 | 505 | .not-found-msgbox{ 506 | margin-bottom: 20px; 507 | width: 300px; 508 | border-color: red; 509 | background-color: red; 510 | padding: 20px; 511 | font-size: 11pt; 512 | color: white; 513 | font-weight: bold; 514 | } 515 | --------------------------------------------------------------------------------