├── db.json ├── .gitignore ├── .DS_Store ├── src └── js │ ├── constants.js │ ├── components │ ├── .DS_Store │ ├── Main.js │ ├── TableData.js │ ├── TableExerciseName.js │ ├── Layout.js │ ├── videos │ │ ├── DipsVideos.js │ │ ├── CurlsVideos.js │ │ ├── PullUpVideos.js │ │ ├── PulldownVideos.js │ │ ├── CalfRaisesVideos.js │ │ ├── FacePullsVideos.js │ │ ├── BentOverRowVideos.js │ │ ├── FarmersWalkVideos.js │ │ ├── HammerCurlsVideos.js │ │ ├── InvertedRowVideos.js │ │ ├── BackExtensionVideos.js │ │ ├── HangingLegRaisesVideos.js │ │ ├── ReverseMachineFlyVideos.js │ │ ├── HighCableChestFlyVideos.js │ │ ├── InclineBenchPressVideos.js │ │ ├── InclineDumbbellPressVideos.js │ │ ├── SquatVideos.js │ │ ├── DeadliftVideos.js │ │ ├── StandingPressVideos.js │ │ └── YouTube.js │ ├── charts │ │ ├── DeadliftChart.js │ │ ├── PulldownChart.js │ │ ├── BackExtensionChart.js │ │ ├── FarmersWalkChart.js │ │ └── DeadliftExercisesChart.js │ ├── Layout │ │ └── Nav.js │ ├── Exercises.js │ └── Exercises │ │ └── Stopwatch.js │ ├── action-creators │ └── exercises.js │ ├── reducers │ ├── root-reducer.js │ └── exercises-reducer.js │ ├── containers │ └── ExercisesContainer.js │ ├── store.js │ └── client.js ├── app.js ├── db ├── database.js └── seed.js ├── server.js ├── index.html ├── webpack.config.js └── package.json /db.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/strength/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | // Exercises 2 | 3 | export const RECEIVE_EXERCISES = 'RECEIVE_EXERCISES'; -------------------------------------------------------------------------------- /src/js/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkosowsk/strength/HEAD/src/js/components/.DS_Store -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const Server = require('./server.js') 2 | const port = (process.env.PORT || 1337) 3 | const app = Server.app() 4 | 5 | app.listen(port) 6 | console.log(`Listening at http://localhost:${port}`) 7 | -------------------------------------------------------------------------------- /src/js/action-creators/exercises.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_EXERCISES } from '../constants'; 2 | // import axios from 'axios'; 3 | 4 | export const receiveExercises = exercises => ({ 5 | type: RECEIVE_EXERCISES, 6 | exercises 7 | }); -------------------------------------------------------------------------------- /src/js/reducers/root-reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import exercisesReducer from './exercises-reducer'; 3 | 4 | const rootReducer = combineReducers({ 5 | exercises: exercisesReducer 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /src/js/components/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {browserHistory} from "react-router"; 3 | 4 | export default class Squat extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

Strength

9 |
10 | ); 11 | } 12 | } -------------------------------------------------------------------------------- /src/js/components/TableData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TableData extends React.Component { 4 | render() { 5 | const cellStyle = {border: '1px solid black'}; 6 | return ( 7 | {this.props.content} 8 | ); 9 | } 10 | } -------------------------------------------------------------------------------- /src/js/containers/ExercisesContainer.js: -------------------------------------------------------------------------------- 1 | import Exercises from '../components/Exercises'; 2 | import { connect } from 'react-redux'; 3 | 4 | const mapStateToProps = (state) => { 5 | return { 6 | exercises: state.exercises.list 7 | }; 8 | }; 9 | 10 | export default connect( 11 | mapStateToProps 12 | )(Exercises); -------------------------------------------------------------------------------- /db/database.js: -------------------------------------------------------------------------------- 1 | var Sequelize = require('sequelize'); 2 | 3 | const db = new Sequelize('postgres://localhost:5432/strength', { 4 | logging: false 5 | }); 6 | 7 | const User = db.define('users', { 8 | username: { 9 | type: Sequelize.STRING, 10 | unique: true 11 | } 12 | }); 13 | 14 | module.exports = { 15 | db, 16 | User 17 | }; -------------------------------------------------------------------------------- /src/js/components/TableExerciseName.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TableExerciseName extends React.Component { 4 | render() { 5 | const cellStyle = {color: 'blue', border: '1px solid black', textDecoration: 'underline'}; 6 | return ( 7 | {this.props.content} 8 | ); 9 | } 10 | } -------------------------------------------------------------------------------- /src/js/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import rootReducer from './reducers/root-reducer'; 3 | // import createLogger from 'redux-logger' 4 | // import thunkMiddleware from 'redux-thunk' 5 | 6 | // const store = createStore(rootReducer, applyMiddleware(createLogger(), thunkMiddleware)) 7 | 8 | const store = createStore(rootReducer); 9 | 10 | export default store; 11 | 12 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const express = require('express') 3 | 4 | module.exports = { 5 | app: function () { 6 | const app = express() 7 | const indexPath = path.join(__dirname, 'index.html') 8 | const publicPath = express.static(path.join(__dirname, 'public')) 9 | 10 | app.use('/public', publicPath) 11 | app.get('/', function (_, res) { res.sendFile(indexPath) }) 12 | 13 | return app 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Strength 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /db/seed.js: -------------------------------------------------------------------------------- 1 | const { db } = require('./database.js'); 2 | 3 | const seedUsers = () => db.Promise.map( 4 | [ 5 | { 6 | username: 'mitch', 7 | }, 8 | { 9 | username: 'russ', 10 | } 11 | ], user => db.model('users').create(user)) 12 | 13 | db.sync() 14 | .then(() => db.sync({ 15 | force: true 16 | })) 17 | .then(seedUsers) 18 | .then((users) => console.log(`Seeded ${users.length} users OK`)) 19 | .catch(error => console.error(error)) 20 | .finally(() => db.close()) -------------------------------------------------------------------------------- /src/js/reducers/exercises-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_EXERCISES 3 | } from '../constants'; 4 | 5 | 6 | const initialExercisesState = { 7 | list: [] 8 | }; 9 | 10 | export default function (state = initialExercisesState, action) { 11 | // console.log(action); 12 | const newState = Object.assign({}, state); 13 | 14 | switch (action.type) { 15 | case RECEIVE_EXERCISES: 16 | newState.list = action.exercises; 17 | break; 18 | 19 | // case RECEIVE_PRODUCT: 20 | // newState.selected = action.product; 21 | // break; 22 | 23 | default: 24 | return state; 25 | } 26 | 27 | return newState; 28 | } -------------------------------------------------------------------------------- /src/js/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Nav from "../components/layout/Nav"; 4 | 5 | export default class Layout extends React.Component { 6 | render() { 7 | const { location } = this.props; 8 | const containerStyle = { 9 | marginTop: "60px" 10 | }; 11 | return ( 12 |
13 |
25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /src/js/components/videos/DipsVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'sM6XUdt1rm4'; 6 | 7 | export default class DipsVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Dips Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/CurlsVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'aEscWJ3dS3w'; 6 | 7 | export default class CurlsVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Curls Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/PullUpVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'VHt8ulfWtzY'; 6 | 7 | export default class PullUpVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Pull-Up Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/PulldownVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'X5n55mMqSUs'; 6 | 7 | export default class PulldownVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Pulldown Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/CalfRaisesVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = '3UWi44yN-wM'; 6 | 7 | export default class CalfRaisesVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Calf Raises Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/FacePullsVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'rep-qVOkqgk'; 6 | 7 | export default class FacePullsVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Face Pulls Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/BentOverRowVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = '9efgcAjQe7E'; 6 | 7 | export default class BentOverRowVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Bent-Over Row Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/FarmersWalkVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'Fkzk_RqlYig'; 6 | 7 | export default class FarmersWalkVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Farmer's Walk Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/HammerCurlsVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'zC3nLlEvin4'; 6 | 7 | export default class HammerCurlsVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Hammer Curls Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/InvertedRowVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'XZV9IwluPjw'; 6 | 7 | export default class InvertedRowVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Inverted Row Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/BackExtensionVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'ph3pddpKzzw'; 6 | 7 | export default class BackExtensionVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Back Extension Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/HangingLegRaisesVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'hdng3Nm1x_E'; 6 | 7 | export default class HangingLegRaisesVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Hanging Leg Raises Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/ReverseMachineFlyVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'o5OvdIVV61M'; 6 | 7 | export default class ReverseMachineFlyVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Reverse Machine Fly Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/HighCableChestFlyVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'Iwe6AmxVf7o'; 6 | 7 | export default class HighCableChestFlyVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

High Cable Chest Fly Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/InclineBenchPressVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'DbFgADa2PL8'; 6 | 7 | export default class InclineBenchPressVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Incline Bench Press Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/videos/InclineDumbbellPressVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = '8iPEnn-ltC8'; 6 | 7 | export default class InclineDumbbellPressVideos extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | videoOneId: videoOneId, 13 | player: null, 14 | }; 15 | 16 | this.onReady = this.onReady.bind(this); 17 | } 18 | 19 | onReady(event) { 20 | this.setState({ 21 | player: event.target, 22 | }); 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

Incline Dumbbell Press Video

29 | 30 |
31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/js/components/charts/DeadliftChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LineChart = require("react-chartjs").Line; 4 | 5 | const lineChartData = { 6 | labels: ['9/3', '9/10', '9/17', '9/24', 7 | '10/1', '10/8', '10/15', '10/22', '10/29', 8 | '11/5', '11/12', '11/19', '11/26', 9 | '12/3', '12/10', '12/17', '12/14'], 10 | datasets: [{ 11 | label: "Deadlift", 12 | strokeColor: 'red', 13 | fillColor: "rgba(0,0,0,0)", 14 | data: [415, 420, 425, 430, 15 | 435, 440, 445, 450, 455, 16 | 385, 390, 395, 400, 17 | 405, 405, 405, 405] 18 | }] 19 | } 20 | 21 | const chartOptions = { 22 | bezierCurve : false 23 | }; 24 | 25 | export default class DeadliftChart extends React.Component { 26 | render() { 27 | return ( 28 |
29 |

Deadlift Over Time

30 | 31 |
32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/components/charts/PulldownChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LineChart = require("react-chartjs").Line; 4 | 5 | const lineChartData = { 6 | labels: ['9/3', '9/10', '9/17', '9/24', 7 | '10/1', '10/8', '10/15', '10/22', '10/29', 8 | '11/5', '11/12', '11/19', '11/26', 9 | '12/3', '12/10', '12/17', '12/14'], 10 | datasets: [{ 11 | label: "Pulldown", 12 | strokeColor: 'orange', 13 | fillColor: "rgba(0,0,0,0)", 14 | data: [145, 145, 145, 145, 15 | 150, 150, 150, 150, 150, 16 | 155, 155, 155, 155, 17 | 160, 160, 160, 160] 18 | }] 19 | } 20 | 21 | const chartOptions = { 22 | bezierCurve : false 23 | }; 24 | 25 | export default class PulldownChart extends React.Component { 26 | render() { 27 | return ( 28 |
29 |

Pulldown Over Time

30 | 31 |
32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/components/charts/BackExtensionChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LineChart = require("react-chartjs").Line; 4 | 5 | const lineChartData = { 6 | labels: ['9/3', '9/10', '9/17', '9/24', 7 | '10/1', '10/8', '10/15', '10/22', '10/29', 8 | '11/5', '11/12', '11/19', '11/26', 9 | '12/3', '12/10', '12/17', '12/14'], 10 | datasets: [{ 11 | label: "Back Extension", 12 | strokeColor: 'green', 13 | fillColor: "rgba(0,0,0,0)", 14 | data: [25, 25, 25, 25, 15 | 25, 25, 25, 25, 25, 16 | 25, 25, 25, 25, 17 | 25, 25, 25, 25] 18 | }] 19 | } 20 | 21 | const chartOptions = { 22 | bezierCurve : false 23 | }; 24 | 25 | export default class BackExtensionChart extends React.Component { 26 | render() { 27 | return ( 28 |
29 |

Back Extension Over Time

30 | 31 |
32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/components/charts/FarmersWalkChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LineChart = require("react-chartjs").Line; 4 | 5 | const lineChartData = { 6 | labels: ['9/3', '9/10', '9/17', '9/24', 7 | '10/1', '10/8', '10/15', '10/22', '10/29', 8 | '11/5', '11/12', '11/19', '11/26', 9 | '12/3', '12/10', '12/17', '12/14'], 10 | datasets: [{ 11 | label: "Farmer's Walk", 12 | strokeColor: 'blue', 13 | fillColor: "rgba(0,0,0,0)", 14 | data: [85, 85, 85, 85, 15 | 85, 85, 85, 85, 85, 16 | 95, 105, 110, 115, 17 | 120, 125, 125, 125] 18 | }] 19 | } 20 | 21 | const chartOptions = { 22 | bezierCurve : false 23 | }; 24 | 25 | export default class FarmersWalkChart extends React.Component { 26 | render() { 27 | return ( 28 |
29 |

Farmer's Walk Over Time

30 | 31 |
32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /src/js/components/videos/SquatVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = 'nFAscG0XUNY'; 6 | const videoTwoId = 'bs_Ej32IYgo'; 7 | 8 | export default class SquatVideos extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | videoOneId: videoOneId, 14 | videoTwoId: videoTwoId, 15 | player: null, 16 | }; 17 | 18 | this.onReady = this.onReady.bind(this); 19 | } 20 | 21 | onReady(event) { 22 | this.setState({ 23 | player: event.target, 24 | }); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |

Squat Videos

31 |

Beginner

32 | 33 |

Advanced

34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var debug = process.env.NODE_ENV !== "production"; 2 | var webpack = require('webpack'); 3 | var path = require('path'); 4 | 5 | module.exports = { 6 | context: path.join(__dirname, "src"), 7 | devtool: debug ? "inline-sourcemap" : null, 8 | entry: "./js/client.js", 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.jsx?$/, 13 | exclude: /(node_modules|bower_components)/, 14 | loader: 'babel-loader', 15 | query: { 16 | presets: ['react', 'es2015', 'stage-0'], 17 | plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'], 18 | } 19 | } 20 | ] 21 | }, 22 | output: { 23 | path: __dirname + "/public/", 24 | filename: "client.min.js" 25 | }, 26 | plugins: debug ? [] : [ 27 | new webpack.optimize.DedupePlugin(), 28 | new webpack.optimize.OccurenceOrderPlugin(), 29 | new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }), 30 | ], 31 | }; -------------------------------------------------------------------------------- /src/js/components/videos/DeadliftVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = '7Q_GnXm7LbI'; 6 | const videoTwoId = 'Y1IGeJEXpF4'; 7 | 8 | export default class DeadliftVideos extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | videoOneId: videoOneId, 14 | videoTwoId: videoTwoId, 15 | player: null, 16 | }; 17 | 18 | this.onReady = this.onReady.bind(this); 19 | } 20 | 21 | onReady(event) { 22 | this.setState({ 23 | player: event.target, 24 | }); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |

Deadlift Videos

31 |

Beginner

32 | 33 |

Advanced

34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/js/components/videos/StandingPressVideos.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import YouTube from './YouTube'; 4 | 5 | const videoOneId = '2yjwXTZQDDI'; 6 | const videoTwoId = 'CnBmiBqp-AI'; 7 | 8 | export default class StandingPressVideos extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | videoOneId: videoOneId, 14 | videoTwoId: videoTwoId, 15 | player: null, 16 | }; 17 | 18 | this.onReady = this.onReady.bind(this); 19 | } 20 | 21 | onReady(event) { 22 | this.setState({ 23 | player: event.target, 24 | }); 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 |

Standing Press Videos

31 |

Beginner

32 | 33 |

Advanced

34 | 35 |
36 | ); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strength", 3 | "version": "0.0.1", 4 | "description": "An app to help weightlifters train", 5 | "engines": { 6 | "node": "5.9.1" 7 | }, 8 | "main": "webpack.config.js", 9 | "scripts": { 10 | "dev": "webpack-dev-server --content-base src", 11 | "build": "webpack --config ./webpack.config.js --progress --colors", 12 | "start": "node app.js" 13 | }, 14 | "dependencies": { 15 | "babel-core": "^6.17.0", 16 | "babel-loader": "^6.2.0", 17 | "babel-plugin-add-module-exports": "^0.1.2", 18 | "babel-plugin-react-html-attrs": "^2.0.0", 19 | "babel-plugin-transform-class-properties": "^6.3.13", 20 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 21 | "babel-preset-es2015": "^6.3.13", 22 | "babel-preset-react": "^6.3.13", 23 | "babel-preset-stage-0": "^6.3.13", 24 | "chart.js": "^1.1.1", 25 | "history": "^1.17.0", 26 | "react": "^0.14.8", 27 | "react-chartjs": "^0.8.0", 28 | "react-dom": "^0.14.8", 29 | "react-redux": "^5.0.2", 30 | "react-router": "^3.0.0", 31 | "redux": "^3.6.0", 32 | "webpack": "^1.12.9", 33 | "webpack-dev-server": "^1.14.1" 34 | }, 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/mkosowsk/strength" 38 | }, 39 | "keywords": [ 40 | "node", 41 | "heroku", 42 | "express" 43 | ], 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /src/js/components/Layout/Nav.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { IndexLink, Link } from "react-router"; 3 | 4 | export default class Nav extends React.Component { 5 | constructor() { 6 | super() 7 | this.state = { 8 | collapsed: true, 9 | }; 10 | } 11 | 12 | toggleCollapse() { 13 | const collapsed = !this.state.collapsed; 14 | this.setState({collapsed}); 15 | } 16 | 17 | render() { 18 | const { location } = this.props; 19 | const { collapsed } = this.state; 20 | const navClass = collapsed ? "collapse" : ""; 21 | 22 | return ( 23 | 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/components/charts/DeadliftExercisesChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LineChart = require("react-chartjs").Line; 4 | 5 | const lineChartData = { 6 | labels: ['9/3', '9/10', '9/17', '9/24', 7 | '10/1', '10/8', '10/15', '10/22', '10/29', 8 | '11/5', '11/12', '11/19', '11/26', 9 | '12/3', '12/10', '12/17', '12/14'], 10 | datasets: [{ 11 | label: "Deadlift", 12 | strokeColor: 'red', 13 | fillColor: "rgba(0,0,0,0)", 14 | data: [415, 420, 425, 430, 15 | 435, 440, 445, 450, 455, 16 | 385, 390, 395, 400, 17 | 405, 405, 405, 405] 18 | }, { 19 | label: "Pulldown", 20 | strokeColor: 'orange', 21 | fillColor: "rgba(0,0,0,0)", 22 | data: [145, 145, 145, 145, 23 | 150, 150, 150, 150, 150, 24 | 155, 155, 155, 155, 25 | 160, 160, 160, 160] 26 | }, { 27 | label: "Back Extension", 28 | strokeColor: 'green', 29 | fillColor: "rgba(0,0,0,0)", 30 | data: [25, 25, 25, 25, 31 | 25, 25, 25, 25, 25, 32 | 25, 25, 25, 25, 33 | 25, 25, 25, 25] 34 | }, { 35 | label: "Farmer's Walk", 36 | strokeColor: 'blue', 37 | fillColor: "rgba(0,0,0,0)", 38 | data: [85, 85, 85, 85, 39 | 85, 85, 85, 85, 85, 40 | 95, 105, 110, 115, 41 | 120, 125, 125, 125] 42 | }] 43 | } 44 | 45 | const chartOptions = { 46 | bezierCurve : false 47 | }; 48 | 49 | export default class DeadliftChart extends React.Component { 50 | render() { 51 | return ( 52 |
53 |

Deadlift Day Exercises Over Time

54 | 55 |
56 | ) 57 | } 58 | } -------------------------------------------------------------------------------- /src/js/components/Exercises.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {browserHistory} from 'react-router'; 3 | 4 | import TableData from "./TableData" 5 | import TableExerciseName from "./TableExerciseName" 6 | 7 | import Stopwatch from "../components/Exercises/Stopwatch"; 8 | 9 | export default function(props) { 10 | const titleAndExercises = props.exercises; 11 | const title = titleAndExercises[0]; 12 | const exercises = titleAndExercises.slice(1); 13 | 14 | const cellStyle = {color: 'blue', border: '1px solid black', textDecoration: 'underline'}; 15 | 16 | return ( 17 |
18 |

{title} Day

19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | { 31 | exercises && exercises.map(exercise => ( 32 | 33 | 37 | 38 | 39 | 43 | 44 | ) 45 | ) 46 | } 47 | 48 |
ExerciseSetsReps browserHistory.push('/deadlift_exercises_chart') }> 27 | Weight 28 |
browserHistory.push('/' + exercise[0].split(' ').join('_').toLowerCase() + '_videos') }> 35 | {exercise[0]} 36 | browserHistory.push('/' + exercise[0].split(' ').join('_').toLowerCase() + '_chart') }> 41 | {exercise[3]} 42 |
49 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/js/components/Exercises/Stopwatch.js: -------------------------------------------------------------------------------- 1 | // from https://jsfiddle.net/vakhtang/j276r2zh/ 2 | 3 | import React from 'react'; 4 | 5 | const leftPad = (width, n) => { 6 | if ((n + '').length > width) { 7 | return n; 8 | } 9 | const padding = new Array(width).join('0'); 10 | return (padding + n).slice(-width); 11 | }; 12 | 13 | export default class Stopwatch extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | ["lap", "update", "reset", "toggle"].forEach((method) => { 18 | this[method] = this[method].bind(this); 19 | }); 20 | 21 | this.state = this.initialState = { 22 | isRunning: false, 23 | lapTimes: [], 24 | timeElapsed: 0, 25 | }; 26 | } 27 | toggle() { 28 | this.setState({isRunning: !this.state.isRunning}, () => { 29 | this.state.isRunning ? this.startTimer() : clearInterval(this.timer) 30 | }); 31 | } 32 | lap() { 33 | const {lapTimes, timeElapsed} = this.state; 34 | this.setState({lapTimes: lapTimes.concat(timeElapsed)}); 35 | } 36 | reset() { 37 | clearInterval(this.timer); 38 | this.setState(this.initialState); 39 | } 40 | startTimer() { 41 | this.startTime = Date.now(); 42 | this.timer = setInterval(this.update, 10); 43 | } 44 | update() { 45 | const delta = Date.now() - this.startTime; 46 | this.setState({timeElapsed: this.state.timeElapsed + delta}); 47 | this.startTime = Date.now(); 48 | } 49 | render() { 50 | const {isRunning, lapTimes, timeElapsed} = this.state; 51 | return ( 52 |
53 | 54 | 57 | 63 | {lapTimes.length > 0 && } 64 |
65 | ); 66 | } 67 | } 68 | 69 | class TimeElapsed extends React.Component { 70 | getUnits() { 71 | const seconds = this.props.timeElapsed / 1000; 72 | return { 73 | min: Math.floor(seconds / 60).toString(), 74 | sec: Math.floor(seconds % 60).toString(), 75 | msec: (seconds % 1).toFixed(3).substring(2) 76 | }; 77 | } 78 | render() { 79 | const units = this.getUnits(); 80 | return ( 81 |
82 | {leftPad(2, units.min)}: 83 | {leftPad(2, units.sec)}. 84 | {units.msec} 85 |
86 | ); 87 | } 88 | } 89 | 90 | class LapTimes extends React.Component { 91 | render() { 92 | const rows = this.props.lapTimes.map((lapTime, index) => 93 | 94 | {index} 95 | 96 | 97 | ); 98 | return ( 99 | 100 | 101 | 102 | 103 | 104 | {rows} 105 |
LapTime
106 | ); 107 | } 108 | } -------------------------------------------------------------------------------- /src/js/client.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import {connect, Provider} from 'react-redux' 4 | import {browserHistory, IndexRoute, Router, Route} from 'react-router'; 5 | 6 | import store from "./store"; 7 | 8 | import Layout from "./components/Layout"; 9 | import Main from "./components/Main"; 10 | 11 | import {receiveExercises} from './action-creators/exercises'; 12 | import ExercisesContainer from "./containers/ExercisesContainer"; 13 | 14 | import SquatVideos from "./components/videos/SquatVideos"; 15 | import StandingPressVideos from "./components/videos/StandingPressVideos"; 16 | import ReverseMachineFlyVideos from "./components/videos/ReverseMachineFlyVideos"; 17 | import CalfRaisesVideos from "./components/videos/CalfRaisesVideos"; 18 | import HangingLegRaisesVideos from "./components/videos/HangingLegRaisesVideos"; 19 | 20 | import InclineBenchPressVideos from "./components/videos/InclineBenchPressVideos"; 21 | import HighCableChestFlyVideos from "./components/videos/HighCableChestFlyVideos"; 22 | import HammerCurlsVideos from "./components/videos/HammerCurlsVideos"; 23 | import FacePullsVideos from "./components/videos/FacePullsVideos"; 24 | import InclineDumbbellPressVideos from "./components/videos/InclineDumbbellPressVideos"; 25 | import DipsVideos from "./components/videos/DipsVideos"; 26 | 27 | import DeadliftVideos from "./components/videos/DeadliftVideos"; 28 | import PulldownVideos from "./components/videos/PulldownVideos"; 29 | import BackExtensionVideos from "./components/videos/BackExtensionVideos"; 30 | import FarmersWalkVideos from "./components/videos/FarmersWalkVideos"; 31 | 32 | import BentOverRowVideos from "./components/videos/BentOverRowVideos"; 33 | import PullUpVideos from "./components/videos/PullUpVideos"; 34 | import CurlsVideos from "./components/videos/CurlsVideos"; 35 | import InvertedRowVideos from "./components/videos/InvertedRowVideos"; 36 | 37 | import DeadliftChart from "./components/charts/DeadliftChart"; 38 | import PulldownChart from "./components/charts/PulldownChart"; 39 | import BackExtensionChart from "./components/charts/BackExtensionChart"; 40 | import FarmersWalkChart from "./components/charts/FarmersWalkChart"; 41 | 42 | import DeadliftExercisesChart from "./components/charts/DeadliftExercisesChart"; 43 | 44 | const squatExercises = [ 45 | ["Squat"], 46 | ["Squat", "4", "5", "315 lbs."], 47 | ["Squat", "1", "7", "315 lbs."], 48 | ["Reverse Machine Fly", "3", "12", "175 lbs."], 49 | ["Lateral/Bent Over Raises", "3", "20", "25 lbs."], 50 | ["Romanian Deadlift", "3", "10", "185 lbs."], 51 | ]; 52 | 53 | const benchExercises = [ 54 | ["Bench"], 55 | ["Incline Bench Press", "3", "10", "205 lbs." ], 56 | ["Decline Bench Press", "2", "8", "225 lbs." ], 57 | ["Hammer Curls", "5", "10", "40 lbs."], 58 | ["Cardio"], 59 | ["Face Pulls", "5", "12", "95 lbs."], 60 | ["Incline Dumbbell Fly", "3", "10", "55 lbs."], 61 | ["Dips", "2", "12"], 62 | ]; 63 | 64 | const deadliftExercises = [ 65 | ["Deadlift"], 66 | ["Deadlift", "4", "6", "405 lbs." ], 67 | ["Pulldown", "3", "12", "180 lbs."], 68 | ["Back Extension", "3", "15", "35 lbs."], 69 | ["Ab Wheel", "3", "10"], 70 | ]; 71 | 72 | const rowExercises = [ 73 | ["Row"], 74 | ["Bent Over Row", "5", "10", "240 lbs." ], 75 | ["Standing Press", "3", "5", "135 lbs."], 76 | ["Pull-up", "3", "10"], 77 | ["Curls", "2", "10", "85 lbs."], 78 | ["Inverted Row", "3", "12"], 79 | ]; 80 | 81 | const onSquatExercisesEnter = () => { 82 | store.dispatch(receiveExercises(squatExercises)); 83 | }; 84 | 85 | const onBenchExercisesEnter = () => { 86 | store.dispatch(receiveExercises(benchExercises)); 87 | }; 88 | 89 | const onDeadliftExercisesEnter = () => { 90 | store.dispatch(receiveExercises(deadliftExercises)); 91 | }; 92 | 93 | const onRowExercisesEnter = () => { 94 | store.dispatch(receiveExercises(rowExercises)); 95 | }; 96 | 97 | const app = document.getElementById('app'); 98 | 99 | ReactDOM.render( 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | , 141 | app); -------------------------------------------------------------------------------- /src/js/components/videos/YouTube.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import isEqual from 'lodash.isequal'; 3 | import youTubePlayer from 'youtube-player'; 4 | 5 | /** 6 | * Check whether a `props` change should result in the video being updated. 7 | * 8 | * @param {Object} prevProps 9 | * @param {Object} props 10 | */ 11 | function shouldUpdateVideo(prevProps, props) { 12 | // A changing video should always trigger an update 13 | if (prevProps.videoId !== props.videoId) { 14 | return true; 15 | } 16 | 17 | // Otherwise, a change in the start/end time playerVars also requires a player 18 | // update. 19 | const prevVars = prevProps.opts.playerVars || {}; 20 | const vars = props.opts.playerVars || {}; 21 | 22 | return prevVars.start !== vars.start || prevVars.end !== vars.end; 23 | } 24 | 25 | /** 26 | * Neutralise API options that only require a video update, leaving only options 27 | * that require a player reset. The results can then be compared to see if a 28 | * player reset is necessary. 29 | * 30 | * @param {Object} opts 31 | */ 32 | function filterResetOptions(opts) { 33 | return { 34 | ...opts, 35 | playerVars: { 36 | ...opts.playerVars, 37 | autoplay: 0, 38 | start: 0, 39 | end: 0, 40 | }, 41 | }; 42 | } 43 | 44 | /** 45 | * Check whether a `props` change should result in the player being reset. 46 | * The player is reset when the `props.opts` change, except if the only change 47 | * is in the `start` and `end` playerVars, because a video update can deal with 48 | * those. 49 | * 50 | * @param {Object} prevProps 51 | * @param {Object} props 52 | */ 53 | function shouldResetPlayer(prevProps, props) { 54 | return !isEqual( 55 | filterResetOptions(prevProps.opts), 56 | filterResetOptions(props.opts) 57 | ); 58 | } 59 | 60 | /** 61 | * Check whether a props change should result in an id or className update. 62 | * 63 | * @param {Object} prevProps 64 | * @param {Object} props 65 | */ 66 | function shouldUpdatePlayer(prevProps, props) { 67 | return ( 68 | prevProps.id === props.id || prevProps.className === props.className 69 | ); 70 | } 71 | 72 | class YouTube extends React.Component { 73 | static propTypes = { 74 | videoId: React.PropTypes.string, 75 | 76 | // custom ID for player element 77 | id: React.PropTypes.string, 78 | 79 | // custom class name for player element 80 | className: React.PropTypes.string, 81 | 82 | // https://developers.google.com/youtube/iframe_api_reference#Loading_a_Video_Player 83 | opts: React.PropTypes.object, 84 | 85 | // event subscriptions 86 | onReady: React.PropTypes.func, 87 | onError: React.PropTypes.func, 88 | onPlay: React.PropTypes.func, 89 | onPause: React.PropTypes.func, 90 | onEnd: React.PropTypes.func, 91 | onStateChange: React.PropTypes.func, 92 | onPlaybackRateChange: React.PropTypes.func, 93 | onPlaybackQualityChange: React.PropTypes.func, 94 | }; 95 | 96 | static defaultProps = { 97 | opts: {}, 98 | onReady: () => {}, 99 | onError: () => {}, 100 | onPlay: () => {}, 101 | onPause: () => {}, 102 | onEnd: () => {}, 103 | onStateChange: () => {}, 104 | onPlaybackRateChange: () => {}, 105 | onPlaybackQualityChange: () => {}, 106 | }; 107 | 108 | /** 109 | * Expose PlayerState constants for convenience. These constants can also be 110 | * accessed through the global YT object after the YouTube IFrame API is instantiated. 111 | * https://developers.google.com/youtube/iframe_api_reference#onStateChange 112 | */ 113 | static PlayerState = { 114 | UNSTARTED: -1, 115 | ENDED: 0, 116 | PLAYING: 1, 117 | PAUSED: 2, 118 | BUFFERING: 3, 119 | CUED: 5, 120 | }; 121 | 122 | constructor(props) { 123 | super(props); 124 | 125 | this.container = null; 126 | this.internalPlayer = null; 127 | } 128 | 129 | componentDidMount() { 130 | this.createPlayer(); 131 | } 132 | 133 | componentDidUpdate(prevProps) { 134 | if (shouldUpdatePlayer(prevProps, this.props)) { 135 | this.updatePlayer(); 136 | } 137 | 138 | if (shouldResetPlayer(prevProps, this.props)) { 139 | this.resetPlayer(); 140 | } 141 | 142 | if (shouldUpdateVideo(prevProps, this.props)) { 143 | this.updateVideo(); 144 | } 145 | } 146 | 147 | componentWillUnmount() { 148 | /** 149 | * Note: The `youtube-player` package that is used promisifies all Youtube 150 | * Player API calls, which introduces a delay of a tick before it actually 151 | * gets destroyed. Since React attempts to remove the element instantly 152 | * this method isn't quick enough to reset the container element. 153 | */ 154 | this.internalPlayer.destroy(); 155 | } 156 | 157 | /** 158 | * https://developers.google.com/youtube/iframe_api_reference#onReady 159 | * 160 | * @param {Object} event 161 | * @param {Object} target - player object 162 | */ 163 | onPlayerReady = event => this.props.onReady(event); 164 | 165 | /** 166 | * https://developers.google.com/youtube/iframe_api_reference#onError 167 | * 168 | * @param {Object} event 169 | * @param {Integer} data - error type 170 | * @param {Object} target - player object 171 | */ 172 | onPlayerError = event => this.props.onError(event); 173 | 174 | /** 175 | * https://developers.google.com/youtube/iframe_api_reference#onStateChange 176 | * 177 | * @param {Object} event 178 | * @param {Integer} data - status change type 179 | * @param {Object} target - actual YT player 180 | */ 181 | onPlayerStateChange = (event) => { 182 | this.props.onStateChange(event); 183 | switch (event.data) { 184 | 185 | case YouTube.PlayerState.ENDED: 186 | this.props.onEnd(event); 187 | break; 188 | 189 | case YouTube.PlayerState.PLAYING: 190 | this.props.onPlay(event); 191 | break; 192 | 193 | case YouTube.PlayerState.PAUSED: 194 | this.props.onPause(event); 195 | break; 196 | 197 | default: 198 | return; 199 | } 200 | }; 201 | 202 | /** 203 | * https://developers.google.com/youtube/iframe_api_reference#onPlaybackRateChange 204 | * 205 | * @param {Object} event 206 | * @param {Float} data - playback rate 207 | * @param {Object} target - actual YT player 208 | */ 209 | onPlayerPlaybackRateChange = event => this.props.onPlaybackRateChange(event); 210 | 211 | /** 212 | * https://developers.google.com/youtube/iframe_api_reference#onPlaybackQualityChange 213 | * 214 | * @param {Object} event 215 | * @param {String} data - playback quality 216 | * @param {Object} target - actual YT player 217 | */ 218 | onPlayerPlaybackQualityChange = event => this.props.onPlaybackQualityChange(event); 219 | 220 | /** 221 | * Initialize the Youtube Player API on the container and attach event handlers 222 | */ 223 | createPlayer = () => { 224 | // do not attempt to create a player server-side, it won't work 225 | if (typeof document === 'undefined') return; 226 | // create player 227 | const playerOpts = { 228 | ...this.props.opts, 229 | // preload the `videoId` video if one is already given 230 | videoId: this.props.videoId, 231 | }; 232 | this.internalPlayer = youTubePlayer(this.container, playerOpts); 233 | // attach event handlers 234 | this.internalPlayer.on('ready', this.onPlayerReady); 235 | this.internalPlayer.on('error', this.onPlayerError); 236 | this.internalPlayer.on('stateChange', this.onPlayerStateChange); 237 | this.internalPlayer.on('playbackRateChange', this.onPlayerPlaybackRateChange); 238 | this.internalPlayer.on('playbackQualityChange', this.onPlayerPlaybackQualityChange); 239 | }; 240 | 241 | /** 242 | * Shorthand for destroying and then re-creating the Youtube Player 243 | */ 244 | resetPlayer = () => this.internalPlayer.destroy().then(this.createPlayer); 245 | 246 | /** 247 | * Method to update the id and class of the Youtube Player iframe. 248 | * React should update this automatically but since the Youtube Player API 249 | * replaced the DIV that is mounted by React we need to do this manually. 250 | */ 251 | updatePlayer = () => { 252 | this.internalPlayer.getIframe().then((iframe) => { 253 | iframe.setAttribute('id', this.props.id); 254 | iframe.setAttribute('class', this.props.className); 255 | }); 256 | }; 257 | 258 | /** 259 | * Call Youtube Player API methods to update the currently playing video. 260 | * Depeding on the `opts.playerVars.autoplay` this function uses one of two 261 | * Youtube Player API methods to update the video. 262 | */ 263 | updateVideo = () => { 264 | if (typeof this.props.videoId === 'undefined' || this.props.videoId === null) { 265 | this.internalPlayer.stopVideo(); 266 | return; 267 | } 268 | 269 | // set queueing options 270 | let autoplay = false; 271 | const opts = { 272 | videoId: this.props.videoId, 273 | }; 274 | if ('playerVars' in this.props.opts) { 275 | autoplay = this.props.opts.playerVars.autoplay === 1; 276 | if ('start' in this.props.opts.playerVars) { 277 | opts.startSeconds = this.props.opts.playerVars.start; 278 | } 279 | if ('end' in this.props.opts.playerVars) { 280 | opts.endSeconds = this.props.opts.playerVars.end; 281 | } 282 | } 283 | 284 | // if autoplay is enabled loadVideoById 285 | if (autoplay) { 286 | this.internalPlayer.loadVideoById(opts); 287 | return; 288 | } 289 | // default behaviour just cues the video 290 | this.internalPlayer.cueVideoById(opts); 291 | }; 292 | 293 | refContainer = (container) => { 294 | this.container = container; 295 | }; 296 | 297 | render() { 298 | return ( 299 | 300 |
301 | 302 | ); 303 | } 304 | } 305 | 306 | export default YouTube; 307 | --------------------------------------------------------------------------------