\n\t\t\t\t\t\t{this.state.exercise.type.attributes.map(attribute =>\n\t\t\t\t\t\t\t
\n\t\t\t\n\t\t);\n\t}\n\n\tisRoutineOlderThanDefaultFetchDate = () => {\n\t\treturn moment(this.state.routine.dateCreated).isBefore(moment().subtract(3, 'months'));\n\t}\n\n\trender() {\n\t\tconst { classes } = this.props;\n\t\tlet logGroups = this.state.exercise.logGroups ?? [];\n\n\t\tlet totalRows = 0;\n\t\tlogGroups.forEach(logGroup => {\n\t\t\ttotalRows += logGroup.logs.length; // add the number of logs in this logGroup\n\t\t\ttotalRows++; // increment for the logGroup itself\n\t\t});\n\n\t\tconst rowHeight = 40; // replace this with the actual height of your rows\n\t\tconst totalHeight = rowHeight + (totalRows * rowHeight) + 67;\n\t\t\n\t\treturn (\n\t\t\t
\n\t\t\t\t
\n\n\t\t\t\t{this.state.logComment && this.state.dropDownCommentAnchorEl ?\n\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.logComment}\n\t\t\t\t\t\t \n\t\t\t\t\t : null\n\t\t\t\t}\n\n\t\t\t\t{this.state.logToEdit && this.state.dropDownMenuAnchorEl ?\n\t\t\t\t\t
\n\t\t\t\t\t\t this.toggleEditableLog(e, this.state.logToEdit)}>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\t this.deleteRoutineExerciseLog(e, this.state.logToEdit)}>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t : null\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t{/*The top row (the head)*/}\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tDate\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t
\n\t\t\t\t\t\t{this.state.exercise.type.attributes.map(attribute =>\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{attribute.name}\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\n\t\t\t\t\t{/*The table body*/}\n\t\t\t\t\t
\n\t\t\t\t\t\t{({width}) => (\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t)}\n\t\t\t\t\t \n\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineExerciseLogsTable);","import React, {PureComponent} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport Grid from \"@mui/material/Grid\";\nimport RoutineExerciseLogNewFormBox from \"./RoutineExerciseLogNewFormBox\";\nimport RoutineExerciseLogsTable from \"./RoutineExerciseLogsTable\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Box from \"@mui/material/Box\";\n\nconst useStyles = theme => ({\n});\n\nclass RoutineExerciseDetailBloopLogsTab extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'routine': The routine.\n\t * 'exercise': The exercise.\n\t * 'isLoading': Loading?\n\t * 'refreshParentFunc': Function to refresh the parent.\n\t * 'switchExerciseBloop': Function to switch bloops.\n\t * 'openFilterByMenu': The function to call to open the filter by menu.\n\t * 'updateSwipeableViewsHeightFunc': Update parent height of view.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine.\n\t\t\t */\n\t\t\troutine: this.props.routine,\n\t\t\t/**\n\t\t\t * The exercise that belongs to the routine.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * Indicates whether this component is in a loading state.\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * If any, this will make the default values set to the ones in this selected log\n\t\t\t */\n\t\t\tselectedLogInBox: null,\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\troutine: nextProps.routine,\n\t\t\texercise: nextProps.exercise,\n\t\t\tisLoading: nextProps.isLoading,\n\t\t\tselectedLogInBox: null, // Reset it to prevent it from happening again after its been set.\n\t\t});\n\t}\n\n\tshouldComponentUpdate(nextProps, nextState, nextContext) {\n\t\treturn nextProps.isShowing || nextProps.exercise.routineExerciseId !== this.state.exercise.routineExerciseId;\n\t}\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\tthis.props.updateSwipeableViewsHeightFunc();\n\t}\n\t\n\tsetSelectedLogInBox = (log) => {\n\t\tthis.setState({\n\t\t\tselectedLogInBox: log,\n\t\t});\n\t\tthis.scrollToTop();\n\t}\n\t\n\tscrollToTop = () => {\n\t\tif (this.refs.routineExerciseLogNewFormBox) {\n\t\t\tthis.refs.routineExerciseLogNewFormBox.scrollIntoView({ behavior: 'smooth' });\n\t\t}\n\t}\n\n\tupdateSwipeableViewsHeightFunc = () => {\n\t\tthis.props.updateSwipeableViewsHeightFunc();\n\t}\n\t\n\trender() {\n\t\treturn (\n
\n {!this.state.routine.isArchived && !this.state.exercise.isArchived ?\n \n \n : null\n }\n\n\t \n\n\t {this.state.isLoading ? : }\n\n \n \n );\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineExerciseDetailBloopLogsTab);","import React, {Component} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport Grid from \"@mui/material/Grid\";\nimport WorkoutSmallNote from \"../../workout/partials/WorkoutSmallNote\";\nimport moment from \"moment\";\nimport {HoursMinutesTimeConverter} from \"../../../core/prettifier/HoursMinutesTimeConverter\";\nimport {ExerciseType} from \"../../../core/models/ExerciseType\";\n\nconst useStyles = theme => ({\n});\n\nclass RoutineExerciseDetailBloopStatsTab extends Component {\n\t/**\n\t * @param props Containing:\n\t * 'exercise': The exercise you want to modify.\n\t * 'isShowing': Is it showing?\n\t * 'updateSwipeableViewsHeightFunc': Update parent height of view.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The exercise that belongs to the routine.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * Has it ever been shown? Similar to 'isShowing' but once it's true, it's never false again.\n\t\t\t */\n\t\t\thasEverBeenShown: false,\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\texercise: nextProps.exercise,\n\t\t\thasEverBeenShown: !this.state.hasEverBeenShown || nextProps.exercise.routineExerciseId !== this.state.exercise.routineExerciseId ? nextProps.isShowing : true, // once it's true, it's never false again, unless the exercise changes.\n\t\t});\n\t}\n\n\tshouldComponentUpdate (nextProps) {\n\t\treturn nextProps.isShowing || nextProps.exercise.routineExerciseId !== this.state.exercise.routineExerciseId; // once it's true, it's never false again, unless the exercise changes.\n\t};\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\tthis.props.updateSwipeableViewsHeightFunc();\n\t}\n\t\n\tgetWeightRepsTypeData = (logGroups) => {\n\t\tlet totalSets = 0;\n\t\tlet totalWorkouts = 0;\n\t\t\n\t\tlet totalWeight = 0;\n\t\tlet totalReps = 0;\n\t\tlet totalVolume = 0;\n\t\t\n\t\tlet averageWeight = 0;\n\t\tlet averageReps = 0;\n\t\tlet averageVolume = 0;\n\t\t\n\t\tlet maxWeightLog = null;\n\t\tlet maxRepsLog = null;\n\t\tlet maxVolumeLog = null;\n\n\t\tlet averageMaxWeight = 0;\n\t\tlet averageMaxReps = 0;\n\t\tlet averageMaxVolume = 0;\n\n\t\tfor (const logGroup of logGroups) {\n\t\t\tfor (const log of logGroup.logs) {\n\t\t\t\ttotalSets++;\n\t\t\t\ttotalWeight += log.values[0];\n\t\t\t\ttotalReps += log.values[1];\n\t\t\t}\n\t\t\t\n\t\t\ttotalWorkouts++;\n\t\t\ttotalVolume += logGroup.totalVolume;\n\n\t\t\tlet topWeightLog = logGroup.logs[logGroup.topWeightLogIdx];\n\t\t\tif (maxWeightLog === null || topWeightLog.values[0] > maxWeightLog.values[0]) {\n\t\t\t\tmaxWeightLog = topWeightLog;\n\t\t\t}\n\t\t\taverageMaxWeight += topWeightLog.values[0];\n\n\t\t\tlet topRepsLog = logGroup.logs[logGroup.topRepsLogIdx];\n\t\t\tif (maxRepsLog === null || topRepsLog.values[1] > maxRepsLog.values[1]) {\n\t\t\t\tmaxRepsLog = topRepsLog;\n\t\t\t}\n\t\t\taverageMaxReps += topRepsLog.values[1];\n\n\t\t\tlet topVolumeLog = logGroup.logs[logGroup.topVolumeLogIdx];\n\t\t\tif (maxVolumeLog === null || topVolumeLog.multiplied > maxVolumeLog.multiplied) {\n\t\t\t\tmaxVolumeLog = topVolumeLog;\n\t\t\t}\n\t\t\taverageMaxVolume += topVolumeLog.multiplied;\n\t\t}\n\n\t\taverageWeight = totalSets > 0 ? totalWeight / totalSets : 0;\n\t\taverageReps = totalSets > 0 ? totalReps / totalSets : 0;\n\t\taverageVolume = totalSets > 0 ? totalVolume / totalSets : 0;\n\n\t\taverageMaxWeight = totalWorkouts > 0 ? averageMaxWeight / totalWorkouts : 0;\n\t\taverageMaxReps = totalWorkouts > 0 ? averageMaxReps / totalWorkouts : 0;\n\t\taverageMaxVolume = totalWorkouts > 0 ? averageMaxVolume / totalWorkouts : 0;\n\n\t\treturn {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalWeight: totalWeight.toFixed(1),\n\t\t\ttotalReps: totalReps.toFixed(0),\n\t\t\ttotalVolume: totalVolume.toFixed(1),\n\t\t\taverageWeight: averageWeight.toFixed(1),\n\t\t\taverageReps: averageReps.toFixed(1),\n\t\t\taverageVolume: averageVolume.toFixed(1),\n\t\t\tmaxWeightLog,\n\t\t\tmaxRepsLog,\n\t\t\tmaxVolumeLog,\n\t\t\taverageMaxWeight: averageMaxWeight.toFixed(1),\n\t\t\taverageMaxReps: averageMaxReps.toFixed(1),\n\t\t\taverageMaxVolume: averageMaxVolume.toFixed(1),\n\t\t};\n\t}\n\t\n\trenderWeightRepsType = () => {\n\t\tconst {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalWeight,\n\t\t\ttotalReps,\n\t\t\ttotalVolume,\n\t\t\taverageWeight,\n\t\t\taverageReps,\n\t\t\taverageVolume,\n\t\t\tmaxWeightLog,\n\t\t\tmaxRepsLog,\n\t\t\tmaxVolumeLog,\n\t\t\taverageMaxWeight,\n\t\t\taverageMaxReps,\n\t\t\taverageMaxVolume,\n\t\t} = this.getWeightRepsTypeData(this.state.exercise.logGroups);\n\n\t\treturn
\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\tgetDistanceTimeTypeData = (logGroups) => {\n\t\tlet totalSets = 0;\n\t\tlet totalWorkouts = 0;\n\n\t\tlet totalDistance = 0;\n\t\tlet totalTime = 0;\n\n\t\tlet averageDistance = 0;\n\t\tlet averageTime = 0;\n\n\t\tlet maxDistanceLog = null;\n\t\tlet maxTimeLog = null;\n\n\t\tlet averageMaxDistance = 0;\n\t\tlet averageMaxTime = 0;\n\n\t\tfor (const logGroup of logGroups) {\n\t\t\tfor (const log of logGroup.logs) {\n\t\t\t\ttotalSets++;\n\t\t\t\ttotalDistance += log.values[0];\n\t\t\t\ttotalTime += log.values[1];\n\t\t\t}\n\n\t\t\ttotalWorkouts++;\n\n\t\t\tlet topDistanceLog = logGroup.logs[logGroup.topDistanceLogIdx];\n\t\t\tif (maxDistanceLog === null || topDistanceLog.values[0] > maxDistanceLog.values[0]) {\n\t\t\t\tmaxDistanceLog = topDistanceLog;\n\t\t\t}\n\t\t\taverageMaxDistance += topDistanceLog.values[0];\n\n\t\t\tlet topTimeLog = logGroup.logs[logGroup.topTimeLogIdx];\n\t\t\tif (maxTimeLog === null || topTimeLog.values[1] > maxTimeLog.values[1]) {\n\t\t\t\tmaxTimeLog = topTimeLog;\n\t\t\t}\n\t\t\taverageMaxTime += topTimeLog.values[1];\n\t\t}\n\n\t\taverageDistance = totalSets > 0 ? totalDistance / totalSets : 0;\n\t\taverageTime = totalSets > 0 ? totalTime / totalSets : 0;\n\n\t\taverageMaxDistance = totalWorkouts > 0 ? averageMaxDistance / totalWorkouts : 0;\n\t\taverageMaxTime = totalWorkouts > 0 ? averageMaxTime / totalWorkouts : 0;\n\n\t\treturn {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalDistance: totalDistance.toFixed(1),\n\t\t\ttotalTime: HoursMinutesTimeConverter.convertFloatToString(totalTime),\n\t\t\taverageDistance: averageDistance.toFixed(1),\n\t\t\taverageTime: HoursMinutesTimeConverter.convertFloatToString(averageTime),\n\t\t\tmaxDistanceLog: maxDistanceLog,\n\t\t\tmaxTimeLog: maxTimeLog,\n\t\t\taverageMaxDistance: averageMaxDistance.toFixed(1),\n\t\t\taverageMaxTime: HoursMinutesTimeConverter.convertFloatToString(averageMaxTime),\n\t\t};\n\t}\n\n\trenderDistanceTimeType = () => {\n\t\tconst {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalDistance,\n\t\t\ttotalTime,\n\t\t\taverageDistance,\n\t\t\taverageTime,\n\t\t\tmaxDistanceLog,\n\t\t\tmaxTimeLog,\n\t\t\taverageMaxDistance,\n\t\t\taverageMaxTime,\n\t\t} = this.getDistanceTimeTypeData(this.state.exercise.logGroups);\n\n\t\treturn
\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t \n\t}\n\n\tgetTimeTypeData = (logGroups) => {\n\t\tlet totalSets = 0;\n\t\tlet totalWorkouts = 0;\n\n\t\tlet totalTime = 0;\n\n\t\tlet averageTime = 0;\n\n\t\tlet maxTimeLog = null;\n\n\t\tlet averageMaxTime = 0;\n\n\t\tfor (const logGroup of logGroups) {\n\t\t\tfor (const log of logGroup.logs) {\n\t\t\t\ttotalSets++;\n\t\t\t\ttotalTime += log.values[0];\n\t\t\t}\n\n\t\t\ttotalWorkouts++;\n\n\t\t\tlet topTimeLog = logGroup.logs[logGroup.topTimeLogIdx];\n\t\t\tif (maxTimeLog === null || topTimeLog.values[0] > maxTimeLog.values[0]) {\n\t\t\t\tmaxTimeLog = topTimeLog;\n\t\t\t}\n\t\t\taverageMaxTime += topTimeLog.values[0];\n\t\t}\n\n\t\taverageTime = totalSets > 0 ? totalTime / totalSets : 0;\n\n\t\taverageMaxTime = totalWorkouts > 0 ? averageMaxTime / totalWorkouts : 0;\n\n\t\treturn {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalTime: HoursMinutesTimeConverter.convertFloatToString(totalTime),\n\t\t\taverageTime: HoursMinutesTimeConverter.convertFloatToString(averageTime),\n\t\t\tmaxTimeLog: maxTimeLog,\n\t\t\taverageMaxTime: HoursMinutesTimeConverter.convertFloatToString(averageMaxTime),\n\t\t};\n\t}\n\n\trenderTimeType = () => {\n\t\tconst {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalTime,\n\t\t\taverageTime,\n\t\t\tmaxTimeLog,\n\t\t\taverageMaxTime,\n\t\t} = this.getTimeTypeData(this.state.exercise.logGroups);\n\n\t\treturn
\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\t\n\tgetRepsTypeData = (logGroups) => {\n\t\tlet totalSets = 0;\n\t\tlet totalWorkouts = 0;\n\t\t\n\t\tlet totalReps = 0;\n\t\t\n\t\tlet averageReps = 0;\n\t\t\n\t\tlet maxRepsLog = null;\n\n\t\tlet averageMaxReps = 0;\n\n\t\tfor (const logGroup of logGroups) {\n\t\t\tfor (const log of logGroup.logs) {\n\t\t\t\ttotalSets++;\n\t\t\t\ttotalReps += log.values[0];\n\t\t\t}\n\t\t\t\n\t\t\ttotalWorkouts++;\n\t\t\t\n\t\t\tlet topRepsLog = logGroup.logs[logGroup.topRepsLogIdx];\n\t\t\tif (maxRepsLog === null || topRepsLog.values[0] > maxRepsLog.values[0]) {\n\t\t\t\tmaxRepsLog = topRepsLog;\n\t\t\t}\n\t\t\taverageMaxReps += topRepsLog.values[0];\n\t\t}\n\n\t\taverageReps = totalSets > 0 ? totalReps / totalSets : 0;\n\n\t\taverageMaxReps = totalWorkouts > 0 ? averageMaxReps / totalWorkouts : 0;\n\t\t\n\t\treturn {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalReps: totalReps.toFixed(0),\n\t\t\taverageReps: averageReps.toFixed(1),\n\t\t\tmaxRepsLog,\n\t\t\taverageMaxReps: averageMaxReps.toFixed(1),\n\t\t};\n\t}\n\t\n\trenderRepsType = () => {\n\t\tconst {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalReps,\n\t\t\taverageReps,\n\t\t\tmaxRepsLog,\n\t\t\taverageMaxReps,\n\t\t} = this.getRepsTypeData(this.state.exercise.logGroups);\n\n\t\treturn
\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\tgetWeightTimeTypeData = (logGroups) => {\n\t\tlet totalSets = 0;\n\t\tlet totalWorkouts = 0;\n\n\t\tlet totalWeight = 0;\n\t\tlet totalTime = 0;\n\n\t\tlet averageWeight = 0;\n\t\tlet averageTime = 0;\n\n\t\tlet maxWeightLog = null;\n\t\tlet maxTimeLog = null;\n\n\t\tlet averageMaxWeight = 0;\n\t\tlet averageMaxTime = 0;\n\n\t\tfor (const logGroup of logGroups) {\n\t\t\tfor (const log of logGroup.logs) {\n\t\t\t\ttotalSets++;\n\t\t\t\ttotalWeight += log.values[0];\n\t\t\t\ttotalTime += log.values[1];\n\t\t\t}\n\n\t\t\ttotalWorkouts++;\n\n\n\t\t\tlet topWeightLog = logGroup.logs[logGroup.topWeightLogIdx];\n\t\t\tif (maxWeightLog === null || topWeightLog.values[0] > maxWeightLog.values[0]) {\n\t\t\t\tmaxWeightLog = topWeightLog;\n\t\t\t}\n\t\t\taverageMaxWeight += topWeightLog.values[0];\n\n\t\t\tlet topTimeLog = logGroup.logs[logGroup.topTimeLogIdx];\n\t\t\tif (maxTimeLog === null || topTimeLog.values[1] > maxTimeLog.values[1]) {\n\t\t\t\tmaxTimeLog = topTimeLog;\n\t\t\t}\n\t\t\taverageMaxTime += topTimeLog.values[1];\n\t\t}\n\n\t\taverageWeight = totalSets > 0 ? totalWeight / totalSets : 0;\n\t\taverageTime = totalSets > 0 ? totalTime / totalSets : 0;\n\n\t\taverageMaxWeight = totalWorkouts > 0 ? averageMaxWeight / totalWorkouts : 0;\n\t\taverageMaxTime = totalWorkouts > 0 ? averageMaxTime / totalWorkouts : 0;\n\n\t\treturn {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalWeight: totalWeight.toFixed(1),\n\t\t\ttotalTime: HoursMinutesTimeConverter.convertFloatToString(totalTime),\n\t\t\taverageWeight: averageWeight.toFixed(1),\n\t\t\taverageTime: HoursMinutesTimeConverter.convertFloatToString(averageTime),\n\t\t\tmaxWeightLog,\n\t\t\tmaxTimeLog: maxTimeLog,\n\t\t\taverageMaxWeight: averageMaxWeight.toFixed(1),\n\t\t\taverageMaxTime: HoursMinutesTimeConverter.convertFloatToString(averageMaxTime),\n\t\t};\n\t}\n\n\trenderWeightTimeType = () => {\n\t\tconst {\n\t\t\ttotalSets,\n\t\t\ttotalWorkouts,\n\t\t\ttotalWeight,\n\t\t\ttotalTime,\n\t\t\taverageWeight,\n\t\t\taverageTime,\n\t\t\tmaxWeightLog,\n\t\t\tmaxTimeLog,\n\t\t\taverageMaxWeight,\n\t\t\taverageMaxTime,\n\t\t} = this.getWeightTimeTypeData(this.state.exercise.logGroups);\n\n\t\treturn
\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t \n\n\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\trender() {\n\t\tif (!this.state.hasEverBeenShown) {\n\t\t\treturn
;\n\t\t}\n\n\t\tif (this.state.exercise.type.id === ExerciseType.WeightAndReps) {\n\t\t\treturn this.renderWeightRepsType();\n\t\t}\n\n\t\tif (this.state.exercise.type.id === ExerciseType.WeightAndTime) {\n\t\t\treturn this.renderWeightTimeType();\n\t\t}\n\n\t\tif (this.state.exercise.type.id === ExerciseType.Reps) {\n\t\t\treturn this.renderRepsType();\n\t\t}\n\n\t\tif (this.state.exercise.type.id === ExerciseType.DistanceAndTime) {\n\t\t\treturn this.renderDistanceTimeType();\n\t\t}\n\n\t\tif (this.state.exercise.type.id === ExerciseType.Time) {\n\t\t\treturn this.renderTimeType();\n\t\t}\n\n\t\treturn
\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineExerciseDetailBloopStatsTab);","import React, {Component} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Button from \"@mui/material/Button\";\nimport Dialog from \"@mui/material/Dialog\";\nimport InputLabel from \"@mui/material/InputLabel\";\nimport Select from \"@mui/material/Select\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport FormControl from \"@mui/material/FormControl\";\nimport TextField from \"@mui/material/TextField\";\nimport Grid from \"@mui/material/Grid\";\nimport InputAdornment from \"@mui/material/InputAdornment\";\nimport {UnitPrettifier} from \"../../../../core/prettifier/UnitPrettifier\";\nimport {Checkbox, Collapse, Divider, FormControlLabel} from \"@mui/material\";\nimport {UrlBuilder} from \"../../../../core/url/UrlBuilder\";\nimport moment from \"moment\";\nimport Typography from \"@mui/material/Typography\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport IconButton from \"@mui/material/IconButton\";\nimport DeleteIcon from \"@mui/icons-material/Delete\";\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport {HoursMinutesTimeConverter} from \"../../../../core/prettifier/HoursMinutesTimeConverter\";\nimport AdapterDateFns from \"@mui/lab/AdapterDateFns\";\nimport {DatePicker, LocalizationProvider, TimePicker} from \"@mui/lab\";\nimport Slide from \"@mui/material/Slide\";\nimport {ExerciseType} from \"../../../../core/models/ExerciseType\";\nimport {ExerciseGoalType} from \"../../../../core/models/ExerciseGoalType\";\n\nconst useStyles = theme => ({\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tformControl: {\n\t\tmargin: theme.spacing(1),\n\t\tminWidth: 250,\n\t},\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nclass GoalNewOrEditFormPopUp extends Component {\n\n\t/**\n\t * @param props Containing:\n\t * 'goal': The goal to edit, if any.\n\t * 'isOpen': Says whether this modal is open.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The exercise this goal is for.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * The goal.\n\t\t\t */\n\t\t\tgoal: this.props.goal,\n\t\t\t/**\n\t\t\t * Is it open?\n\t\t\t */\n\t\t\tisOpen: this.props.isOpen,\n\t\t\t/**\n\t\t\t * The selected goal-type. The different types depend on the exercise type. {@see renderGoalTypesMenu}\n\t\t\t */\n\t\t\tselectedGoalTypeId: this.props.goal ? this.props.goal.type.id : null,\n\t\t\t/**\n\t\t\t * The goal deadline, if any.\n\t\t\t */\n\t\t\tdeadline: this.props.goal && this.props.goal.deadline ? this.props.goal.deadline : null,\n\t\t\t/**\n\t\t\t * The first value.\n\t\t\t */\n\t\t\tvalue1: this.props.goal ? this.props.goal.value1 : 0,\n\t\t\t/**\n\t\t\t * The second value.\n\t\t\t */\n\t\t\tvalue2: this.props.goal ? this.props.goal.value2 : 0,\n\t\t\t/**\n\t\t\t * The goal description, if any.\n\t\t\t */\n\t\t\tdescription: this.props.goal ? this.props.goal.description : '',\n\t\t\t/**\n\t\t\t * Is it routine specific? If so, we'll include the routineExerciseId in the goal.\n\t\t\t */\n\t\t\tisRoutineSpecific: this.props.goal && this.props.goal.routineExerciseId ? this.props.goal.routineExerciseId : false,\n\t\t\t/**\n\t\t\t * Is it loading?\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\texercise: nextProps.exercise,\n\t\t\t\n\t\t\tgoal: nextProps.goal,\n\t\t\tisOpen: nextProps.isOpen,\n\t\t\tselectedGoalTypeId: nextProps.goal ? nextProps.goal.type.id : null,\n\t\t\tdeadline: nextProps.goal && nextProps.goal.deadline ? nextProps.goal.deadline : null,\n\t\t\tvalue1: nextProps.goal ? (nextProps.goal.values[0] ?? 0) : 0,\n\t\t\tvalue2: nextProps.goal ? (nextProps.goal.values[1] ?? 0) : 0,\n\t\t\tdescription: nextProps.goal ? nextProps.goal.description : '',\n\t\t\tisRoutineSpecific: nextProps.goal && nextProps.goal.routineExerciseId ? nextProps.goal.routineExerciseId : false,\n\t\t\t\n\t\t\tisDeleteConfirmationDialogOpen: false,\n\t\t});\n\t}\n\n\tupdateSelectedGoalTypeIdFromDropdownInputSelection = (event) => {\n\t\tthis.setState({\n\t\t\tselectedGoalTypeId: event.target.value,\n\t\t\tvalue1: 0, // Reset these values if the type changed.\n\t\t\tvalue2: 0,\n\t\t});\n\t}\n\n\tupdateStateInputValues = (event) => {\n\t\tthis.setState({\n\t\t\t[event.target.name]: event.target.value\n\t\t});\n\t}\n\n\tupdateRoutineSpecificCheckboxInputValue = (e) => {\n\t\tthis.setState({\n\t\t\tisRoutineSpecific: e.target.checked,\n\t\t});\n\t}\n\n\t/**\n\t * This is similar to {@see updateStateInputValues} but handles the date.\n\t *\n\t * @param date\n\t */\n\thandleDateChange = (date) => {\n\t\tthis.setState({\n\t\t\tdeadline: date\n\t\t});\n\t};\n\n\tsubmitWithEnter = (e) => {\n\t\tif (e.key === 'Enter' && !this.state.isLoading) {\n\t\t\tthis.submit(e);\n\t\t}\n\t}\n\n\tsubmit = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t});\n\n\t\tlet exerciseGoalToSave;\n\t\tif (this.state.goal) {\n\t\t\texerciseGoalToSave = {\n\t\t\t\tid: this.state.goal.id,\n\t\t\t\troutineExerciseId: this.state.isRoutineSpecific ? this.state.exercise.routineExerciseId : null,\n\t\t\t\tdescription: this.state.description,\n\t\t\t\tdeadline: this.state.deadline ? moment(this.state.deadline).format('L, h:mm:ss A') : null,\n\t\t\t\tdateModified: moment().format('L, h:mm:ss A'),\n\t\t\t};\n\t\t} else {\n\t\t\texerciseGoalToSave = {\n\t\t\t\ttype: {\n\t\t\t\t\tid: this.state.selectedGoalTypeId,\n\t\t\t\t},\n\t\t\t\texerciseId: this.state.exercise.id,\n\t\t\t\troutineExerciseId: this.state.isRoutineSpecific ? this.state.exercise.routineExerciseId : null,\n\t\t\t\tdescription: this.state.description,\n\t\t\t\tdeadline: this.state.deadline ? moment(this.state.deadline).format('L, h:mm:ss A') : null,\n\t\t\t\tdateCreated: moment().format('L, h:mm:ss A'),\n\t\t\t};\n\t\t}\n\n\t\tlet values = [this.state.value1];\n\t\tif (this.state.value2) {\n\t\t\tvalues.push(this.state.value2);\n\t\t}\n\t\texerciseGoalToSave['values'] = values;\n\n\t\tawait fetch(UrlBuilder.goals.exerciseGoalsApi(), {\n\t\t\tmethod: this.state.goal ? 'PUT' : 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(exerciseGoalToSave)\n\t\t}).then(() => this.props.closeSelfFunc(true));\n\n\t\tthis.setState({\n\t\t\tisLoading: false,\n\t\t});\n\t}\n\n\topenGoalDeleteConfirmationDialog = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisDeleteConfirmationDialogOpen: isOpen\n\t\t})\n\t}\n\n\trenderDeleteConfirmationDialog = (e) => {\n\t\tif (!this.state.goal) {\n\t\t\treturn null;\n\t\t}\n\t\tif (!this.state.isDeleteConfirmationDialogOpen) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn
this.openGoalDeleteConfirmationDialog(false)} TransitionComponent={Transition}>\n\t\t\t{\"Are you sure you want to delete this goal?\"} \n\t\t\t\n\t\t\t\t\n\t\t\t\t\tThis cannot be undone.\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t this.openGoalDeleteConfirmationDialog(false)}>\n\t\t\t\t\tCancel\n\t\t\t\t \n\t\t\t\t this.props.deleteGoalFunc(e, this.state.goal)} color=\"primary\" autoFocus>\n\t\t\t\t\tDelete\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\trenderGoalTypesMenu = () => {\n\t\tswitch (this.state.exercise.type.id) {\n\t\t\tcase ExerciseType.WeightAndReps:\n\t\t\t\treturn
\n\t\t\t\t\tWeight & Reps \n\t\t\t\t\tWeight \n\t\t\t\t\tReps \n\t\t\t\t ;\n\t\t\tcase ExerciseType.Reps:\n\t\t\t\treturn
\n\t\t\t\t\tReps \n\t\t\t\t ;\n\t\t\tcase ExerciseType.DistanceAndTime:\n\t\t\t\treturn
\n\t\t\t\t\tDistance & Time \n\t\t\t\t\tDistance \n\t\t\t\t\tTime \n\t\t\t\t ;\n\t\t\tcase ExerciseType.Time:\n\t\t\t\treturn
\n\t\t\t\t\tTime \n\t\t\t\t ;\n\t\t\tcase ExerciseType.WeightAndTime:\n\t\t\t\treturn
\n\t\t\t\t\tWeight & Time \n\t\t\t\t\tWeight \n\t\t\t\t\tTime \n\t\t\t\t ;\n\t\t}\n\t}\n\n\trenderTextField = (label, key, unitLabel = null) => {\n\t\treturn
{unitLabel},\n\t\t\t}}\n\t\t\tonChange={this.updateStateInputValues}\n\t\t\tonKeyPress={this.submitWithEnter}\n\t\t\tstyle={{maxWidth: 150}}\n\t\t\tdisabled={this.state.goal && this.state.goal.dateCompleted}\n\t\t/>;\n\t}\n\n\t/**\n\t * @param key\n\t * @param val A string representing a dateTime object.\n\t */\n\tupdateTimeAttributeValue = (key, val) => {\n\t\tthis.setState({\n\t\t\t[key]: (HoursMinutesTimeConverter.convertDateStringToFloat(val)),\n\t\t});\n\t}\n\t\n\trenderTimeField = (label, key) => {\n\t\treturn \n\t\t\t this.updateTimeAttributeValue(key, val)}\n\t\t\t\tdisabled={this.state.goal && this.state.goal.dateCompleted}\n\t\t\t\trenderInput={(params) => }\n\t\t\t/>\n\t\t ;\n\t}\n\n\trenderGoalTypeValues = () => {\n\t\tswitch (this.state.selectedGoalTypeId) {\n\t\t\tcase ExerciseGoalType.WeightAndRepsGoal:\n\t\t\t\treturn <>\n\t\t\t\t\t{this.renderTextField('Weight', 'value1', UnitPrettifier.weight())}\n\t\t\t\t\t \n\t\t\t\t\t{this.renderTextField('Reps', 'value2', 'reps')}\n\t\t\t\t>;\n\t\t\tcase ExerciseGoalType.WeightGoal:\n\t\t\t\treturn this.renderTextField('Weight', 'value1', UnitPrettifier.weight());\n\t\t\tcase ExerciseGoalType.RepsGoal:\n\t\t\t\treturn this.renderTextField('Reps', 'value1', 'reps');\n\t\t\tcase ExerciseGoalType.DistanceAndTimeGoal:\n\t\t\t\treturn <>\n\t\t\t\t\t{this.renderTextField('Distance', 'value1', UnitPrettifier.distance())}\n\t\t\t\t\t \n\t\t\t\t\t{this.renderTimeField('Time', 'value2')}\n\t\t\t\t>;\n\t\t\tcase ExerciseGoalType.DistanceGoal:\n\t\t\t\treturn this.renderTextField('Distance', 'value1', UnitPrettifier.distance());\n\t\t\tcase ExerciseGoalType.TimeGoal:\n\t\t\t\treturn this.renderTimeField('Time', 'value1');\n\t\t\tcase ExerciseGoalType.WeightAndTimeGoal:\n\t\t\t\treturn <>\n\t\t\t\t\t{this.renderTextField('Weight', 'value1', UnitPrettifier.weight())}\n\t\t\t\t\t \n\t\t\t\t\t{this.renderTimeField('Time', 'value2')}\n\t\t\t\t>;\n\t\t}\n\t}\n\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n this.props.closeSelfFunc(false)} fullWidth TransitionComponent={Transition}>\n {this.state.goal ? 'Edit Goal' : 'Create Goal'} \n \n \n \n \n \n Goal Type\n \n {this.renderGoalTypesMenu()}\n \n \n \n \n \n \n \n \n {this.renderGoalTypeValues()}\n \n \n\t \n }\n />\n\t \n \n \n \n \n \n \n \n }\n label={Make routine-specific }\n />\n \n \n {this.state.isRoutineSpecific ? \n 'Unchecking this will make this goal available for any routine that has this exercise.' :\n 'Checking this will make this goal available for only this specific routine-exercise combination.'\n }\n \n \n \n\n {this.renderDeleteConfirmationDialog()}\n\n \n \n \n {this.state.goal ?\n this.openGoalDeleteConfirmationDialog(true)} size=\"large\">\n \n : null\n }\n this.props.closeSelfFunc(false)} className={classes.button}>\n Close\n \n \n {this.state.isLoading ?\n \n Loading...\n
:\n 'Save'\n }\n \n \n \n );\n\t}\n}\n\nexport default withStyles(useStyles)(GoalNewOrEditFormPopUp);","import React, {Component} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport WeightGoalCard from \"./Goals/WeightGoalCard\";\nimport Grid from \"@mui/material/Grid\";\nimport WeightRepsGoalCard from \"./Goals/WeightRepsGoalCard\";\nimport {LinearProgress, Typography} from \"@mui/material\";\nimport Box from \"@mui/material/Box\";\nimport AddIcon from '@mui/icons-material/Add';\nimport Button from \"@mui/material/Button\";\nimport GoalNewOrEditFormPopUp from \"./Goals/GoalNewOrEditFormPopUp\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport RepsGoalCard from \"./Goals/RepsGoalCard\";\nimport CardActionArea from \"@mui/material/CardActionArea\";\nimport DistanceTimeGoalCard from \"./Goals/DistanceTimeGoalCard\";\nimport TimeGoalCard from \"./Goals/TimeGoalCard\";\nimport WeightTimeGoalCard from \"./Goals/WeightTimeGoalCard\";\nimport Collapse from \"@mui/material/Collapse\";\nimport DistanceGoalCard from \"./Goals/DistanceGoalCard\";\nimport {ExerciseGoalType} from \"../../../core/models/ExerciseGoalType\";\nimport GoalCard from \"./Goals/GoalCard\";\n\nconst useStyles = theme => ({\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tmedia: {\n\t\theight: 120,\n\t},\n});\n\nclass RoutineExerciseDetailBloopGoalsTab extends Component {\n\t/**\n\t * @param props Containing:\n\t * 'exercise': The exercise you want to modify.\n\t * 'isShowing': Is it showing?\n\t * 'updateSwipeableViewsHeightFunc': Update parent height of view.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine.\n\t\t\t */\n\t\t\troutine: this.props.routine,\n\t\t\t/**\n\t\t\t * The exercise that belongs to the routine.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * Is it showing?\n\t\t\t */\n\t\t\tisShowing: false,\n\t\t\t/**\n\t\t\t * Is the popup to edit/create a goal open?\n\t\t\t */\n\t\t\tisGoalNewOrEditFormPopUpOpen: false,\n\t\t\t/**\n\t\t\t * The goals to show (fetched from the API).\n\t\t\t */\n\t\t\tgoals: null,\n\t\t\t/**\n\t\t\t * Is the fetching loading?\n\t\t\t */\n\t\t\tisFetchingLoading: false,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (!nextProps.isShowing) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tthis.setState({\n\t\t\texercise: nextProps.exercise,\n\t\t\tgoals: nextProps.exercise.routineExerciseId !== this.state.exercise.routineExerciseId ? null : this.state.goals,\n\t\t\tisShowing: nextProps.isShowing\n\t\t});\n\t\t\n\t\tthis.refresh(nextProps.exercise);\n\t}\n\n\tshouldComponentUpdate(nextProps) {\n\t\treturn nextProps.isShowing || nextProps.exercise.routineExerciseId !== this.state.exercise.routineExerciseId;\n\t};\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\tthis.props.updateSwipeableViewsHeightFunc();\n\t}\n\t\n\topenGoalNewOrEditFormPopUp = (isOpen, goalToEdit = null, shouldRefresh = true) => {\n\t\tthis.setState({\n\t\t\tisGoalNewOrEditFormPopUpOpen: isOpen,\n\t\t\tgoalToEdit: goalToEdit,\n\t\t});\n\t\tif (!isOpen && shouldRefresh) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.goals === null;\n\t}\n\t\n\trefresh = async (exercise = null) => {\n\t\tthis.setState({\n\t\t\tisFetchingLoading: true\n\t\t});\n\n\t\tlet goals = await this.fetchGoalsForExercise(exercise ? exercise : this.state.exercise);\n\t\t\n\t\tthis.setState({\n\t\t\tgoals: goals,\n\t\t\tisFetchingLoading: false,\n\t\t});\n\t}\n\t\n\tfetchGoalsForExercise = async(exercise) => {\n\t\treturn await fetch(UrlBuilder.goals.exerciseGoalsApi(exercise.id, exercise.routineExerciseId))\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => response['data'])\n\t}\n\n\tdeleteGoal = async (event, goal) => {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n\t\tawait fetch(UrlBuilder.goals.goalsApi(goal.id), {\n\t\t\tmethod: 'DELETE',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t})\n\t\t\t.then(() => this.openGoalNewOrEditFormPopUp(false, null, true));\n\t}\n\t\n\trenderGoalCards = () => {\n\t\tif (this.hasNeverBeenFetched()) {\n\t\t\treturn ;\n\t\t}\n\n\t\tif (this.state.goals.length === 0) {\n\t\t\treturn \n\t\t\t\t\n\t\t\t\t\tNo goals found.\n\t\t\t\t \n\t\t\t \n\t\t}\n\n\t\treturn \n\t\t\t{this.state.goals.map(goal =>\n\t\t\t\t this.openGoalNewOrEditFormPopUp(true, goal)}>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t)}\n\t\t \n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tif (!this.state.isShowing) {\n\t\t\treturn
;\n\t\t}\n\n\t\treturn <>\n\t\t\t this.openGoalNewOrEditFormPopUp(false, null, shouldRefresh)}\n\t\t\t/>\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t
this.openGoalNewOrEditFormPopUp(true)}\n\t\t\t\t\tstartIcon={ }\n\t\t\t\t\tdisabled={this.state.routine.isArchived || this.state.exercise.isArchived}\n\t\t\t\t>\n\t\t\t\t\tNew Goal\n\t\t\t\t \n\t\t\t\n\n\t\t\t
\n\n\t\t\t
\n\t\t\t\t \n\t\t\t \n\n\t\t\t{this.renderGoalCards()}\n\t\t>\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineExerciseDetailBloopGoalsTab);","import React, {PureComponent} from 'react'\nimport SwipeableViews from 'react-swipeable-views';\nimport withStyles from '@mui/styles/withStyles';\nimport Container from \"@mui/material/Container\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Dialog from \"@mui/material/Dialog\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport CloseIcon from '@mui/icons-material/Close';\nimport Slide from \"@mui/material/Slide\";\nimport Tabs from \"@mui/material/Tabs\";\nimport Tab from \"@mui/material/Tab\";\nimport Snackbar from \"@mui/material/Snackbar\";\nimport Typography from \"@mui/material/Typography\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport RadioButtonCheckedIcon from \"@mui/icons-material/RadioButtonChecked\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport RoutineExerciseDetailBloopGraphsTab from \"./RoutineExerciseDetailBloopGraphsTab\";\nimport Fade from \"@mui/material/Fade\";\nimport RoutineExerciseDetailBloopLogsTab from \"./RoutineExerciseDetailBloopLogsTab\";\nimport Box from \"@mui/material/Box\";\nimport {UserContext} from \"../../../core/UserContext\";\nimport Menu from \"@mui/material/Menu\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport moment from \"moment\";\nimport ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport ListIcon from \"@mui/icons-material/List\";\nimport ArrowForwardIcon from \"@mui/icons-material/ArrowForward\";\nimport Avatar from \"@mui/material/Avatar\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport {Button, CircularProgress, Divider, ListItemIcon, Popover} from \"@mui/material\";\nimport Fab from \"@mui/material/Fab\";\nimport SortIcon from '@mui/icons-material/Sort';\nimport PlayArrowIcon from \"@mui/icons-material/PlayArrow\";\nimport RoutineExerciseDetailBloopStatsTab from \"./RoutineExerciseDetailBloopStatsTab\";\nimport RoutineExerciseDetailBloopGoalsTab from \"./RoutineExerciseDetailBloopGoalsTab\";\nimport ListItemAvatar from \"@mui/material/ListItemAvatar\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport ListItem from \"@mui/material/ListItem\";\nimport {DatePrettifier} from \"../../../core/prettifier/DatePrettifier\";\nimport Backdrop from \"@mui/material/Backdrop\";\nimport SupersetBadge from \"../../core/custom/SupersetBadge\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport { red } from '@mui/material/colors';\nimport {ToggleButton, ToggleButtonGroup} from \"@mui/lab\";\nimport HelpIcon from \"@mui/icons-material/Help\";\nimport Papa from 'papaparse';\nimport { saveAs } from 'file-saver';\nimport DownloadIcon from '@mui/icons-material/Download';\n\nconst useStyles = theme => ({\n\ttitle: {\n\t\tmarginTop: 8,\n\t\tflex: 100,\n\t\tfontSize: 18,\n\t},\n\tcontainer: {\n\t\twidth: '100%',\n\t\tpadding: 0,\n\t\tbackgroundColor: theme.palette.background.default,\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\ttabMenu: {\n\t\tcolor: \"rgba(241,241,241,0.89)\",\n\t},\n\tbottomAppBar: {\n\t\ttop: 'auto',\n\t\tbottom: 0,\n\t\tzIndex: 1,\n\t},\n\tsmallAvatar: {\n\t\twidth: theme.spacing(4),\n\t\theight: theme.spacing(4),\n\t},\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\tfabButton: {\n\t\tposition: 'absolute',\n\t\ttop: -22,\n\t\tleft: 0,\n\t\tright: 0,\n\t\tmargin: '0 auto',\n\t},\n\t'@keyframes flicker': {\n\t\tfrom: {\n\t\t\topacity: 1,\n\t\t},\n\t\tto: {\n\t\t\topacity: 0.6,\n\t\t},\n\t},\n\tflicker: {\n\t\tanimationName: '$flicker',\n\t\tanimationDuration: '1000ms',\n\t\tanimationIterationCount: 'infinite',\n\t\tanimationDirection: 'alternate',\n\t\tanimationTimingFunction: 'ease-in-out',\n\t\tcolor: red[300],\n\t},\n\tpopOverText: {\n\t\tpadding: theme.spacing(2),\n\t}\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nfunction TabPanel(props) {\n\tconst { children, value, index, ...other } = props;\n\n\treturn (\n\t\t
\n\t\t\t\n\t\t\t\t{children}\n\t\t\t \n\t\t
\n\t);\n}\n\nfunction a11yProps(index) {\n\treturn {\n\t\tid: `full-width-tab-${index}`,\n\t\t'aria-controls': `full-width-tabpanel-${index}`,\n\t};\n}\n\n/**\n * A card that can be popped up into fullscreen mode.\n */\nclass RoutineExerciseDetailBloop extends PureComponent {\n\tstatic displayName = RoutineExerciseDetailBloop.name;\n\n\t/**\n\t * @param props Containing:\n\t * 'routine': The routine.\n\t * 'exercise': The exercise that belongs to the routine.\n\t * 'isOpen': Is this bloop open?\n\t * 'closeSelfFunc': Close self.\n\t * 'updateParentExercise': The function the parent's exercise.\n\t * 'switchExerciseBloop': The function to switch which exercise is displayed here.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine.\n\t\t\t */\n\t\t\troutine: this.props.routine,\n\t\t\t/**\n\t\t\t * The exercise that belongs to the routine.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * Indicates whether this component is in a loading state.\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * Indicates whether the bloop is open.\n\t\t\t */\n\t\t\tisOpen: this.props.isOpen,\n\t\t\t/**\n\t\t\t * The tab that's currently being shown. Default: 2 -> Logs.\n\t\t\t */\n\t\t\tvalue: 2,\n\t\t\t/**\n\t\t\t * The message to show for the toast notification. Null if it shouldn't show. Initializing\n\t\t\t * this will cause the toast notification to show up.\n\t\t\t */\n\t\t\ttoastNotificationMessage: null,\n\t\t\t/**\n\t\t\t * The app-bar menu El.\n\t\t\t */\n\t\t\tappBarMenuAnchorEl: null,\n\t\t\t/**\n\t\t\t * The date-range fetched. Default goes here.\n\t\t\t */\n\t\t\tdateRangeFetched: '3_months',\n\t\t\t/**\n\t\t\t * The dropdown menu for all routine-exercises dropdown.\n\t\t\t */\n\t\t\tdropDownMenuAnchorEl: null,\n\t\t\t/**\n\t\t\t * Should we show all the logs for this routine-exercise? in other words, do we show the logs for this exercise for all routine exercises there?\n\t\t\t */\n\t\t\tshouldShowAllLogs: false,\n\t\t\t/**\n\t\t\t * The help text for the tooltip explaining what the Default/All button does.\n\t\t\t */\n\t\t\tshowingAllPopupHelpText: null,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tlet state = {\n\t\t\troutine: nextProps.routine,\n\t\t\texercise: nextProps.exercise ? nextProps.exercise : this.state.exercise, // No good reason to undefine the exercise. This allows us to have nicer animations.\n\t\t\tisOpen: nextProps.isOpen,\n\t\t\tshouldShowAllLogs: false, // Reset this.\n\t\t\tshowingAllPopupHelpText: null,\n\t\t\tdropDownMenuAnchorEl: null,\n\t\t};\n\n\t\tif (this.state.exercise?.id != nextProps.exercise?.id) { // Reset if you switch exercises.\n\t\t\tstate['value'] = 2; // Reset to the Logs tab.\n\t\t\tstate['dateRangeFetched'] = '3_months'; \n\t\t}\n\t\t\n\t\tthis.setState(state);\n\t}\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\t// Detects the browser back button and closes the detail popup.\n\t\twindow.onpopstate = e => {\n\t\t\tthis.props.closeSelfFunc();\n\t\t}\n\t}\n\n\t/**\n\t * @param event\n\t * @param reason\n\t */\n\tcloseToastNotification = (event, reason) => {\n\t\tif (reason === 'clickaway') {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\ttoastNotificationMessage: null,\n\t\t});\n\t};\n\n\trefresh = async (withMessage = null, time = null, showAllLogs = false, isQuickFetch = false) => {\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t\tappBarMenuAnchorEl: null,\n\t\t});\n\n\t\tlet datetime = (moment()).subtract(3, 'months');\n\t\tlet dateRangeFetched = '3_months'; // We always reset when we refresh, to avoid slowdowns.\n\t\tif (time !== null) {\n\t\t\tswitch (time) {\n\t\t\t\tcase '1_month':\n\t\t\t\t\tdatetime = (moment()).subtract(1, 'month');\n\t\t\t\t\tdateRangeFetched = '1_month';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '3_months':\n\t\t\t\t\tdatetime = (moment()).subtract(3, 'months');\n\t\t\t\t\tdateRangeFetched = '3_months';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '6_months':\n\t\t\t\t\tdatetime = (moment()).subtract(6, 'months');\n\t\t\t\t\tdateRangeFetched = '6_months';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '1_year':\n\t\t\t\t\tdatetime = (moment()).subtract(1, 'year');\n\t\t\t\t\tdateRangeFetched = '1_year';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'all_time':\n\t\t\t\t\tdatetime = null;\n\t\t\t\t\tdateRangeFetched = 'all_time';\n\t\t\t}\n\t\t} else {\n\t\t\tif (isQuickFetch && this.context.user?.activeWorkout) { // Double-check: quick fetches can only be done on active workouts.\n\t\t\t\tdatetime = moment(this.context.user.activeWorkout.dateTimeStarted); // Get as least logs as possible: only for the current workout (note that we format this to 'L', so we only care about the current date, not time).\n\t\t\t\tdateRangeFetched = this.state.dateRangeFetched;\n\t\t\t}\n\t\t}\n\t\t\n\t\tawait fetch(\n\t\t\tUrlBuilder.routine.routineExercisesApi(this.state.exercise.routineExerciseId, datetime ? datetime.format('L') : null, showAllLogs)\n\t\t)\n\t\t\t.then(response => response.json())\n\t\t\t.then(exercise => {\n\t\t\t\tif (isQuickFetch) {\n\t\t\t\t\tlet currentExerciseLogGroups = this.state.exercise.logGroups ?? [];\n\t\t\t\t\tif (\n\t\t\t\t\t\t// We can only replace the first log group (by definition a quick fetch is only done on current non-past workouts).\n\t\t\t\t\t\tcurrentExerciseLogGroups[0] && \n\t\t\t\t\t\texercise.logGroups[0] && \n\t\t\t\t\t\t// Make sure that the dates are equal just in case.\n\t\t\t\t\t\tmoment(currentExerciseLogGroups[0].date).format('YYYY-MM-DD') === moment(exercise.logGroups[0].date).format('YYYY-MM-DD')\n\t\t\t\t\t) {\n\t\t\t\t\t\t// Replace the appropriate log group.\n\t\t\t\t\t\tcurrentExerciseLogGroups[0] = exercise.logGroups[0];\n\t\t\t\t\t\texercise.logGroups = currentExerciseLogGroups;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tthis.props.updateParentExercise(exercise); // This will pass the new exercise to the bloop, so not need to set it here.\n\t\t\t\tthis.setState({\n\t\t\t\t\tisLoading: false,\n\t\t\t\t\ttoastNotificationMessage: withMessage,\n\t\t\t\t\tdateRangeFetched: dateRangeFetched,\n\t\t\t\t\tshouldShowAllLogs: showAllLogs,\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\tthis.setState({\n\t\t\t\t\tisLoading: false,\n\t\t\t\t\ttoastNotificationMessage: withMessage,\n\t\t\t\t});\n\t\t\t});\n\t}\n\n\thandleChange = (event, newValue) => {\n\t\tthis.setState({\n\t\t\tvalue: newValue\n\t\t});\n\t};\n\n\thandleChangeIndex = (index) => {\n\t\tthis.setState({\n\t\t\tvalue: index\n\t\t});\n\t};\n\n\topenAppBarMenu = (event) => {\n\t\tthis.setState({\n\t\t\tappBarMenuAnchorEl: event.currentTarget\n\t\t});\n\t}\n\t\n\tcloseAppBarMenu = () => {\n\t\tthis.setState({\n\t\t\tappBarMenuAnchorEl: null\n\t\t});\n\t}\n\n\t/**\n\t * https://github.com/oliviertassinari/react-swipeable-views/issues/339\n\t * We have to manually tell the swipeable views to update its height, since the height can change.\n\t */\n\tupdateSwipeableViewsHeight = () => {\n\t\tif (this.refs.swipeableViewsRef) {\n\t\t\tthis.refs.swipeableViewsRef.updateHeight();\n\t\t}\n\t}\n\n\topenDropdownMenu = (event) => {\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: event.currentTarget\n\t\t});\n\t};\n\n\tcloseDropdownMenu = () => {\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: null\n\t\t});\n\t};\n\n\tgoToNextExercise = () => {\n\t\tfor (let i = 0; i < this.state.routine.exercises.length; i++) {\n\t\t\tlet exercise = this.state.routine.exercises[i];\n\t\t\tif (this.state.exercise.routineExerciseId == exercise.routineExerciseId) {\n\t\t\t\tthis.props.switchExerciseBloop(this.state.routine.exercises[i + 1]);\n\t\t\t}\n\t\t}\n\t}\n\n\tgoToPreviousExercise = () => {\n\t\tfor (let i = 0; i < this.state.routine.exercises.length; i++) {\n\t\t\tlet exercise = this.state.routine.exercises[i];\n\t\t\tif (this.state.exercise.routineExerciseId == exercise.routineExerciseId) {\n\t\t\t\tthis.props.switchExerciseBloop(this.state.routine.exercises[i - 1]);\n\t\t\t}\n\t\t}\n\t}\n\n\tstartWorkout = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisWorkoutLoading: true,\n\t\t});\n\n\t\tlet workout = {\n\t\t\tdateTimeStarted: moment().format('L, h:mm:ss A')\n\t\t};\n\n\t\tlet res = await fetch(UrlBuilder.workout.startWorkoutApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(workout)\n\t\t}).then(res => res.json());\n\n\t\tthis.context.setUser(res.data);\n\n\t\tthis.setState({\n\t\t\tisWorkoutLoading: false,\n\t\t\ttoastNotificationMessage: 'Workout started. Tracking exercises.'\n\t\t});\n\t}\n\t\n\tchangeShowAllLogs = (showAll) => {\n\t\tthis.setState({\n\t\t\tshouldShowAllLogs: showAll,\n\t\t});\n\t}\n\t\n\tshowShowAllPopupHelpText = (e) => {\n\t\tthis.setState({\n\t\t\tshowingAllPopupHelpText: e.currentTarget,\n\t\t});\n\t}\n\n\tcloseShowAllPopupHelpText = () => {\n\t\tthis.setState({\n\t\t\tshowingAllPopupHelpText: null,\n\t\t});\n\t}\n\n\tdownloadLogs = () => {\n\t\tlet logs = this.state.exercise.logGroups.map(group => group.logs).flat();\n\t\t\n\t\tconst formattedLogs = logs.map(log => {\n\t\t\tlet map = {\n\t\t\t\tDate: moment(log.dateCreated).format('DD-MM-YYYY HH:mm:ss'),\n\t\t\t};\n\n\t\t\tif (this.state.exercise.type.attributes[0]) {\n\t\t\t\tmap[this.state.exercise.type.attributes[0].name] = log.values[0];\n\t\t\t}\n\n\t\t\tif (this.state.exercise.type.attributes[1]) {\n\t\t\t\tmap[this.state.exercise.type.attributes[1].name] = log.values[1];\n\t\t\t}\n\t\t\t\n\t\t\treturn map;\n\t\t});\n\n\t\t// unparse data to CSV\n\t\tconst csv = Papa.unparse(formattedLogs);\n\t\t\n\t\t// create a blob and download\n\t\tconst blob = new Blob([csv], {type: \"text/csv;charset=utf-8\"});\n\t\tsaveAs(blob, this.state.exercise.name + \".csv\");\n\t\t\n\t\tthis.closeAppBarMenu();\n\t}\n\n\trenderShowingAllPopupHelpText = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn
\n\t\t\t\n\t\t\t\tBy default, the logs fetched are the logs done for this particular exercise in this particular routine (i.e. this routine-exercise combination). \n\t\t\t\tYou can alternatively display 'all' logs for this exercise in general, meaning that all logs for this exercise will be fetched, regardless of which routine they are in. \n\t\t\t \n\t\t \n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tif (!this.state.exercise) {\n\t\t\treturn
;\n\t\t}\n\t\t\n\t\tlet dateRangeFetchedText = '1 Month';\n\t\tswitch (this.state.dateRangeFetched) {\n\t\t\tcase '3_months':\n\t\t\t\tdateRangeFetchedText = '3 Months'\n\t\t\t\tbreak;\n\t\t\tcase '6_months':\n\t\t\t\tdateRangeFetchedText = '6 Months'\n\t\t\t\tbreak;\n\t\t\tcase '1_year':\n\t\t\t\tdateRangeFetchedText = '1 Year'\n\t\t\t\tbreak;\n\t\t\tcase 'all_time':\n\t\t\t\tdateRangeFetchedText = 'All Time'\n\t\t}\n\n\t\tif (this.state.value === 0) {\n\t\t\tdateRangeFetchedText = 'All Time';\n\t\t}\n\t\t\n\t\treturn (\n
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.exercise.name} - {this.state.routine.name}\n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t : }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{dateRangeFetchedText}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t{this.renderShowingAllPopupHelpText()}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t Options: \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\tCSV \n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t\t Show: \n\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t this.changeShowAllLogs(v === 'all')}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t this.refresh(null, this.state.dateRangeFetched, false)}>Default \n\t\t\t\t\t\t\t\t\t this.refresh(null, this.state.dateRangeFetched, true)}>All \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t Filter by: \n\t\t\t\t\t\t\t this.refresh(null, '1_month', this.state.shouldShowAllLogs)} selected={this.state.dateRangeFetched === '1_month'}> 1 month \n\t\t\t\t\t\t\t this.refresh(null, '3_months', this.state.shouldShowAllLogs)} selected={this.state.dateRangeFetched === '3_months'}> 3 months \n\t\t\t\t\t\t\t this.refresh(null, '6_months', this.state.shouldShowAllLogs)} selected={this.state.dateRangeFetched === '6_months'}> 6 months \n\t\t\t\t\t\t\t this.refresh(null, '1_year', this.state.shouldShowAllLogs)} selected={this.state.dateRangeFetched === '1_year'}> 1 year \n\t\t\t\t\t\t\t this.refresh(null, 'all_time', this.state.shouldShowAllLogs)} selected={this.state.dateRangeFetched === 'all_time'}> All time \n\t\t\t\t\t\t\n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.context.user && this.context.user.activeWorkout ?\n\t\t\t\t\t\t\t\t this.context.openActiveWorkoutBloop(true)}/> :\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\tExercise Switcher\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t{this.state.routine.exercises.map((exercise, idx) =>\n\t\t\t\t\t\t\t\t this.props.switchExerciseBloop(exercise)}\n\t\t\t\t\t\t\t\t disabled={exercise.routineExerciseId == this.state.exercise.routineExerciseId}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{exercise.superset ?\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{exercise.name.charAt(0).toUpperCase()}\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{exercise.name.charAt(0).toUpperCase()}\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{exercise.dateLastLogged ? DatePrettifier.prettifyLastTrained(new Date(exercise.dateLastLogged)) : '–'}\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t} \n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t\n\t\t\t\n );\n\t}\n}\n\nRoutineExerciseDetailBloop.contextType = UserContext;\n\nexport default withStyles(useStyles)(withRouter(RoutineExerciseDetailBloop));","import React, {Component} from 'react';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport withStyles from '@mui/styles/withStyles';\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport Button from \"@mui/material/Button\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport moment from \"moment\";\nimport Slide from \"@mui/material/Slide\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport Typography from \"@mui/material/Typography\";\nimport IconButton from \"@mui/material/IconButton\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Divider from \"@mui/material/Divider\";\nimport Checkbox from \"@mui/material/Checkbox\";\nimport AddIcon from '@mui/icons-material/Add';\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Fade from \"@mui/material/Fade\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport ExerciseNewOrEditFormPopUp from \"../../exercise/partials/ExerciseNewOrEditFormPopUp\";\nimport {InputBase, ListItem, ListItemAvatar, ListItemText, Paper} from \"@mui/material\";\nimport Avatar from \"@mui/material/Avatar\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport List from \"@mui/material/List\";\nimport SearchIcon from \"@mui/icons-material/Search\";\nimport Grid from \"@mui/material/Grid\";\n\nconst useStyles = theme => ({\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\ttitle: {\n\t\tmarginLeft: theme.spacing(2),\n\t\tflex: 100,\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\troot: {\n\t\twidth: '100%',\n\t},\n\tsmall: {\n\t\twidth: theme.spacing(4),\n\t\theight: theme.spacing(4),\n\t},\n\tsearchBoxRoot: {\n\t\tpadding: '2px 4px',\n\t\tdisplay: 'flex',\n\t\talignItems: 'center',\n\t\twidth: '100%',\n\t},\n\tsearchBoxInput: {\n\t\tmarginLeft: theme.spacing(1),\n\t\tflex: 1,\n\t},\n\ticonButton: {\n\t\tpadding: 10,\n\t},\n\tdivider: {\n\t\theight: 28,\n\t\tmargin: 4,\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\n/**\n * Bloop to add new exercises to a routine.\n */\nclass RoutineExercisesEditFormBloop extends Component {\n\t/**\n\t * @param props Containing:\n\t * 'isOpen': Says whether this modal is open.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine to edit, if any. Otherwise the form will try to create one instead.\n\t\t\t */\n\t\t\troutine: this.props.routine,\n\t\t\t/**\n\t\t\t * 'isOpen': Says whether this bloop is open.\n\t\t\t */\n\t\t\tisOpen: false,\n\t\t\t/**\n\t\t\t * Used as a component 'is loading' indicator.\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Is it currently being submitted?\n\t\t\t */\n\t\t\tisSubmissionLoading: false,\n\t\t\t/**\n\t\t\t * All the exercises that we have available for the user.\n\t\t\t */\n\t\t\tfetchedExercises: [],\n\t\t\t/**\n\t\t\t * The new exercises selected to be added to the routine from the list of fetchedExercises.\n\t\t\t */\n\t\t\tselectedExercises: {},\n\t\t\t/**\n\t\t\t * The exercises belonging to the routine\n\t\t\t */\n\t\t\troutineExercises : this.props.routine ? this.props.routine.exercises : [],\n\t\t\t/**\n\t\t\t * Show the add exercise popup\n\t\t\t */\n\t\t\tshowAddExercisePopUp: false,\n\t\t\t/**\n\t\t\t * The search box text.\n\t\t\t */\n\t\t\tsearchBoxText: '',\n\t\t};\n }\n \n componentWillReceiveProps(nextProps, nextContext) {\n\t if (this.state.isOpen !== Boolean(nextProps.isOpen)) {\n\t\t this.setState({\n\t\t\t isOpen: Boolean(nextProps.isOpen),\n\t\t\t routine: nextProps.routine,\n\t\t\t selectedExercises: {},\n\t\t });\n\t\t if (nextProps.isOpen) {\n\t\t this.refresh(); // Only refresh when it was closed before and now its opening.\n\t\t }\n\t }\n }\n\n submitWithEnter = (e) => {\n if (e.key==='Enter') {\n this.submit(e);\n }\n }\n\n\trefresh = () => {\n\t\tthis.fetchExercises();\n\t}\n\t\n\tfetchExercises = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\t\t\n\t\tconst exercisesResponse = await fetch(UrlBuilder.exercise.exercisesApi());\n\t\tlet fetchedExercises = await exercisesResponse.json();\n\n\t\t// A new exercise was added because the list got bigger by 1, so let's pre-select it (kinda hacky) :)\n\t\tif (this.state.fetchedExercises && (fetchedExercises.length - 1 === this.state.fetchedExercises.length)) {\n\t\t\tlet newSelectedExercises = this.state.selectedExercises;\n\t\t\tnewSelectedExercises[fetchedExercises[fetchedExercises.length - 1].id] = true;\n\t\t\t\n\t\t\tthis.setState({\n\t\t\t\tfetchedExercises: fetchedExercises,\n\t\t\t\tisLoading: false,\n\t\t\t\tselectedExercises: newSelectedExercises,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\tfetchedExercises: fetchedExercises,\n\t\t\tisLoading: false\n\t\t});\n\t}\n\n\tsubmit = async (event) => {\n\t\tevent.preventDefault();\n\t\t\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet routineExercises = [];\n\t\t\n\t\tlet sortNumber = this.state.routine && this.state.routine.exercises[this.state.routine.exercises.length - 1] ? \n\t\t\tthis.state.routine.exercises[this.state.routine.exercises.length - 1].sortNumber + 1 : \n\t\t\t0; // If routine exists, take the biggest one which should be the last one in the list.\n\t\t\n\t\tfor (let i = 0; i < this.state.fetchedExercises.length; i++) {\n\t\t\tlet exercise = this.state.fetchedExercises[i];\n\t\t\tif (this.state.selectedExercises[exercise.id]) {\n\t\t\t\troutineExercises.push({\n\t\t\t\t\tid: exercise.id,\n\t\t\t\t\tsortNumber: sortNumber,\n\t\t\t\t});\n\t\t\t\tsortNumber++;\n\t\t\t}\n\t\t}\n\n\t\tlet routineToSave = {\n\t\t\tid: this.state.routine.id,\n\t\t\texercises: routineExercises,\n\t\t};\n\t\t\n\t\tawait fetch(UrlBuilder.routine.routinesApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(routineToSave)\n\t\t})\n\t\t\t.then(response => response.json())\n\t\t\t.then(routine => this.props.closeSelfFunc(true));\n\n\t\tthis.setState({\n\t\t\tselectedExercises: {}, // reset\n\t\t\tisSubmissionLoading: false,\n\t\t});\n\t}\n\n\tupdateSearchBoxText = (value) => {\n\t\tthis.setState({\n\t\t\tsearchBoxText: value,\n\t\t});\n\t}\n\t\n\tgetSearchedAvailableExercises = () => {\n\t\tlet textToSearch = this.state.searchBoxText;\n\n\t\tif (this.state.searchBoxText.trim() === '') {\n\t\t\treturn this.state.fetchedExercises;\n\t\t}\n\n\t\ttextToSearch = textToSearch.toLowerCase();\n\t\t\n\t\treturn this.state.fetchedExercises.filter(exercise =>\n\t\t\texercise.name.toLowerCase().includes(textToSearch)\n\t\t);\n\t}\n\t\n\tupdateStateCheckboxInputValues = (id) => {\n\t\tlet newSelectedExercises = this.state.selectedExercises;\n\t\tnewSelectedExercises[id] = !Boolean(newSelectedExercises[id]);\n\t\t\n\t\tthis.setState({\n\t\t\tselectedExercises: newSelectedExercises\n\t\t});\n\t}\n\n\ttoggleCloseAllPopups = (shouldRefresh = true) => {\n\t\tthis.setState({\n\t\t\tshowAddExercisePopUp: false,\n\t\t});\n\t\tif (shouldRefresh) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\n\ttoggleOpenAddExercisePopup = () => {\n\t\tthis.setState({\n\t\t\tshowAddExercisePopUp: true\n\t\t});\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tlet searchedAvailableExercises = this.getSearchedAvailableExercises();\n\t\t\n\t\treturn (\n this.props.closeSelfFunc(false)} TransitionComponent={Transition}>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tAdd Exercises to {this.state.routine.name}\n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t
this.props.closeSelfFunc(false)}\n size=\"large\">\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \t\n\t\t\t\t\t\t\n\t\t\t\t\t\t this.updateSearchBoxText(e.target.value)}\n\t\t\t\t\t\t\tvalue={this.state.searchBoxText}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t\n\t\t\t\t\t\t{this.state.searchBoxText ?\n\t\t\t\t\t\t\t this.updateSearchBoxText('')}\n size=\"large\">\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t : null\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t \n\n\t\t\t\t\t\n\t\t\t\t\t\t{searchedAvailableExercises.map(exercise => {\n\t\t\t\t\t\t\treturn this.updateStateCheckboxInputValues(exercise.id)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{exercise.name.charAt(0).toUpperCase() ?? '?'}\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t})}\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\t\t\t\t\t\t{searchedAvailableExercises.length === 0 ?\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tNo exercises found. \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t: null\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t Add a new exercise \n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t this.props.closeSelfFunc(false)}>Close \n\t\t\t\t\t\n\t\t\t\t\t\t{this.state.isSubmissionLoading ?\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t Loading...\n\t\t\t\t\t\t\t
:\n\t\t\t\t\t\t\t'Add To Routine ' + `(${Object.values(this.state.selectedExercises).filter(w => w).length})`\n\t\t\t\t\t\t}\n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t
\n\t\t\t\n );\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineExercisesEditFormBloop);","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport Typography from \"@mui/material/Typography\";\nimport Dialog from \"@mui/material/Dialog\";\nimport TextField from \"@mui/material/TextField\";\nimport moment from \"moment\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport Container from \"@mui/material/Container\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Button from \"@mui/material/Button\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport IconButton from \"@mui/material/IconButton\";\nimport DeleteIcon from '@mui/icons-material/Delete';\nimport Slide from \"@mui/material/Slide\";\nimport ColorSelector from \"../../core/utils/ColorSelector\";\nimport {blue} from \"@mui/material/colors\";\n\nconst useStyles = theme => ({\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nclass RoutineSupersetNewOrEditMenuPopUp extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'routine': The routine that a superset is being added to.\n\t * 'name': The name of the superset.\n\t * 'exercise': The exercise to be added to the superset.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine that a superset is being added to.\n\t\t\t */\n\t\t\troutine: props.routine,\n\t\t\t/**\n\t\t\t * The name of the superset (can be passed by parent).\n\t\t\t */\n\t\t\tname: props.superset ? props.superset.name : props.name,\n\t\t\t/**\n\t\t\t * The superset to edit, if any.\n\t\t\t */\n\t\t\tsuperset: props.superset,\n\t\t\t/**\n\t\t\t * The exercise to be added to the superset.\n\t\t\t */\n\t\t\texercise: props.exercise,\n\t\t\t/**\n\t\t\t * Is this component loading?\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * The color.\n\t\t\t */\n\t\t\tcolor: props.superset ? props.superset.color : blue[500],\n\t\t\t/**\n\t\t\t * Is the custom color picker open?\n\t\t\t */\n\t\t\tcustomColorPickAnchorEl: null,\n\t\t};\n\t}\n\t\n\tsubmit = async (e) => {\n\t\te.preventDefault();\n\t\t\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t});\n\n\t\tlet superset;\n\n\t\tif (this.state.superset) {\n\t\t\tsuperset = {\n\t\t\t\tid: this.state.superset.id,\n\t\t\t\tname: this.state.name,\n\t\t\t\tcolor: this.state.color,\n\t\t\t\tdateModified: moment().format('L, h:mm:ss A')\n\t\t\t};\n\t\t} else {\n\t\t\tsuperset = {\n\t\t\t\tname: this.state.name,\n\t\t\t\troutineId: this.state.routine.id,\n\t\t\t\tcolor: this.state.color,\n\t\t\t\tdateCreated: moment().format('L, h:mm:ss A'),\n\t\t\t\troutineExercises: [\n\t\t\t\t\t{\n\t\t\t\t\t\tid: this.state.exercise.routineExerciseId,\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\tlet res = await fetch(UrlBuilder.routine.supersetsApi(), {\n\t\t\tmethod: this.state.superset ? 'PUT' : 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(superset)\n\t\t});\n\n\t\tthis.props.closeSelfFunc(true);\n\t}\n\n\tsubmitWithEnter = (e) => {\n\t\tif (e.key === 'Enter') {\n\t\t\tthis.submit(e);\n\t\t}\n\t}\n\t\n\tdeleteSelf = async (supersetId) => {\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t});\n\n\t\tawait fetch(UrlBuilder.routine.supersetsApi(supersetId), {\n\t\t\tmethod: 'DELETE',\n\t\t});\n\n\t\tthis.props.closeSelfFunc(true);\n\t}\n\n\tupdateStateInputValues = (event) => {\n\t\tthis.setState({\n\t\t\t[event.target.name]: event.target.value\n\t\t});\n\t}\n\t\n\tupdateStateInputColorValue = (color) => {\n\t\tthis.setState({\n\t\t\tcolor: color,\n\t\t});\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n
this.props.closeSelfFunc(false)} open TransitionComponent={Transition}>\n \n \n \n {this.state.superset ? 'Edit Superset' : 'New Superset'}\n \n \n \n\n \n \n \n \n \n Color:\n \n \n \n \n \n\n \n {this.state.superset ?\n this.deleteSelf(this.state.superset.id)} size=\"large\">\n \n : null\n }\n \n this.props.closeSelfFunc(false)}>Close \n \n {this.state.isLoading ?\n \n Save\n
:\n 'Save'\n }\n \n \n \n ); \n\t}\n}\n\nexport default withStyles(useStyles)(RoutineSupersetNewOrEditMenuPopUp);","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {UserLocalStorage} from \"../../../core/storage/UserLocalStorage\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport Typography from \"@mui/material/Typography\";\nimport Dialog from \"@mui/material/Dialog\";\nimport List from \"@mui/material/List\";\nimport ListItem from \"@mui/material/ListItem\";\nimport Skeleton from '@mui/material/Skeleton';\nimport Box from \"@mui/material/Box\";\nimport Grid from \"@mui/material/Grid\";\nimport ListItemAvatar from \"@mui/material/ListItemAvatar\";\nimport {Avatar, IconButton, LinearProgress, ListItemSecondaryAction} from \"@mui/material\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport RoutineSupersetNewOrEditMenuPopUp from \"./RoutineSupersetNewOrEditMenuPopUp\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Button from \"@mui/material/Button\";\nimport CheckCircleIcon from '@mui/icons-material/CheckCircle';\nimport {green} from \"@mui/material/colors\";\nimport AddIcon from '@mui/icons-material/Add';\nimport EditIcon from '@mui/icons-material/Edit';\nimport LinkIcon from \"@mui/icons-material/Link\";\nimport Slide from \"@mui/material/Slide\";\n\nconst useStyles = theme => ({\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\tcheck: {\n\t\tcolor: green[400],\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nclass RoutineSupersetMenuPopUp extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'routine': The routine.\n\t * 'isOpen': Says whether this popup is open.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine.\n\t\t\t */\n\t\t\troutine: this.props.routine,\n\t\t\t/**\n\t\t\t * The exercise that wants to be added to a superset.\n\t\t\t */\n\t\t\texercise: this.props.exercise,\n\t\t\t/**\n\t\t\t * Is it open?\n\t\t\t */\n\t\t\tisOpen: this.props.isOpen,\n\t\t\t/**\n\t\t\t * Is this component loading?\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * The supersets to show.\n\t\t\t */\n\t\t\tsupersets: UserLocalStorage.get(UrlBuilder.routine.supersetsApi(this.props.routine.id)) ?? null,\n\t\t\t/**\n\t\t\t * The superset to edit, if any.\n\t\t\t */\n\t\t\tsupersetToEdit: null,\n\t\t\t/**\n\t\t\t * Is the new superset pop up open?\n\t\t\t */\n\t\t\tisNewSupersetPopUpOpen: false,\n\t\t\t/**\n\t\t\t * Has it ever opened?\n\t\t\t */\n\t\t\thasOpenedAtLeastOnce: false,\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\troutine: nextProps.routine,\n\t\t\texercise: nextProps.exercise ? nextProps.exercise : this.state.exercise, // Can not set it as null.\n\t\t\tisOpen: nextProps.isOpen,\n\t\t\thasOpenedAtLeastOnce: nextProps.isOpen ? true : this.state.hasOpenedAtLeastOnce,\n\t\t});\n\n\t\tif (nextProps.isOpen) {\n\t\t\tthis.fetch();\n\t\t}\n\t}\n\t\n\tfetch = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tlet response = await fetch(UrlBuilder.routine.supersetsApi(this.state.routine.id));\n\t\tresponse = await response.json();\n\t\tconst supersets = response['data'] ?? [];\n\n\t\tthis.setState({\n\t\t\tsupersets: supersets,\n\t\t\tisLoading: false,\n\t\t});\n\n\t\tUserLocalStorage.set(UrlBuilder.routine.supersetsApi(this.state.routine.id), supersets ?? []);\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.supersets === null;\n\t}\n\n\tmoveRoutineExerciseToSuperSet = async (routineExerciseId, superset) => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\t\t\n\t\tlet supersetId = superset.id;\n\t\tfor (const routineExercise of superset.routineExercises) {\n\t\t\tif (routineExercise.id === routineExerciseId) {\n\t\t\t\tsupersetId = null; // If the routine-exercise is already in the superset, then just remove it (deselect action).\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tlet routineExercise = {\n\t\t\tid: routineExerciseId,\n\t\t\tsuperSetId: supersetId,\n\t\t};\n\n\t\tawait fetch(UrlBuilder.routine.routineExercisesApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(routineExercise)\n\t\t});\n\n\t\tawait this.fetch();\n\t}\n\t\n\texerciseIsInSuperset = (superset) => {\n\t\tif (!this.state.exercise) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tfor (const routineExercise of superset.routineExercises) {\n\t\t\tif (routineExercise.id === this.state.exercise.routineExerciseId) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\t\n\trenderLoadingDialog = () => {\n\t\treturn (\n
\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t
\n );\n\t}\n\n\trenderNoSupersetsDialog = () => {\n\t\treturn <>\n
\n\n \n \n You have not added any supersets.\n \n \n >;\n\t}\n\n\trenderSupersets = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n \n {this.state.supersets.map((superset, idx) => (\n this.moveRoutineExerciseToSuperSet(this.state.exercise.routineExerciseId, superset)}>\n \n \n \n \n \n \n\n \n {this.exerciseIsInSuperset(superset) && \n \n \n \n }\n this.openNewSupersetPopup(true, superset)} size=\"large\">\n \n \n \n \n ))}\n
\n );\n\t}\n\t\n\topenNewSupersetPopup = (isOpen, superset = null, shouldReload = false) => {\n\t\tthis.setState({\n\t\t\tisNewSupersetPopUpOpen: isOpen,\n\t\t\tsupersetToEdit: superset,\n\t\t});\n\n\t\tif (shouldReload) {\n\t\t\tthis.fetch();\n\t\t}\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tif (!this.state.hasOpenedAtLeastOnce) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet content;\n\t\tif (this.hasNeverBeenFetched()) {\n\t\t\tcontent = this.renderLoadingDialog();\n\t\t} else if (this.state.supersets.length === 0) {\n\t\t\tcontent = this.renderNoSupersetsDialog();\n\t\t} else {\n\t\t\tcontent = this.renderSupersets();\n\t\t}\n\n\t\treturn <>\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tAdd Superset: {this.state.exercise?.name}\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t{this.state.isLoading ? : }\n\n\t\t\t\t{content}\n\n\t\t\t\t\n\t\t\t\t\t} variant=\"contained\" color='primary' className={classes.button} onClick={() => this.openNewSupersetPopup(true)}>New \n\n\t\t\t\t\t\n\n\t\t\t\t\t
this.props.closeSelfFunc(false)}>Close \n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t{this.state.isNewSupersetPopUpOpen ?\n\t\t\t\t
this.openNewSupersetPopup(false, null, shouldReload)}\n\t\t\t\t/> : null\n\t\t\t}\n\t\t>\n\t}\n}\n\nexport default withStyles(useStyles)(RoutineSupersetMenuPopUp);","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport {Link as RouterLink} from 'react-router-dom';\nimport {UserLocalStorage} from \"../../../core/storage/UserLocalStorage\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Dialog from \"@mui/material/Dialog\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport Typography from \"@mui/material/Typography\";\nimport List from \"@mui/material/List\";\nimport ListItem from \"@mui/material/ListItem\";\nimport Skeleton from '@mui/material/Skeleton';\nimport Link from \"@mui/material/Link\";\nimport ListItemAvatar from \"@mui/material/ListItemAvatar\";\nimport {Avatar, Card} from \"@mui/material\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport Popover from \"@mui/material/Popover\";\nimport IconButton from \"@mui/material/IconButton\";\nimport HelpIcon from '@mui/icons-material/Help';\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Fade from \"@mui/material/Fade\";\nimport Slide from \"@mui/material/Slide\";\n\nconst useStyles = theme => ({\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tpopOverText: {\n\t\tpadding: theme.spacing(2),\n\t}\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\n/**\n * A card that can be popped up into fullscreen mode.\n */\nclass MoveRoutineExerciseDialog extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'exercise': The exercise to move.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The exercise to move to another routine.\n\t\t\t */\n\t\t\texercise: props.exercise,\n\t\t\t/**\n\t\t\t * The current routine.\n\t\t\t */\n\t\t\troutine: props.routine,\n\t\t\t/**\n\t\t\t * All the routines (to pick to where to move the exercise to).\n\t\t\t */\n\t\t\troutines: UserLocalStorage.get(UrlBuilder.routine.routinesApi(null, true)) ?? null,\n\t\t\t/**\n\t\t\t * Is it loading?\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Where to show the help popup.\n\t\t\t */\n\t\t\thelpPopupAnchorEl: null,\n\t\t};\n\t}\n\t\n\tcomponentDidMount() {\n\t\tthis.fetch();\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\texercise: nextProps.exercise,\n\t\t\troutine: nextProps.routine,\n\t\t});\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.routines === null;\n\t}\n\n\tfetch = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tconst response = await fetch(UrlBuilder.routine.routinesApi(null, true));\n\t\tconst routines = await response.json();\n\t\tthis.setState({\n\t\t\troutines: routines,\n\t\t\tisLoading: false\n\t\t});\n\n\t\tUserLocalStorage.set(UrlBuilder.routine.routinesApi(null, true), routines ?? []);\n\t}\n\t\n\tmoveExerciseToRoutine = async (routine) => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\t\t\n\t\tlet routineExerciseToUpdate = {\n\t\t\tid: this.state.exercise.routineExerciseId,\n\t\t\troutineId: routine.id,\n\t\t};\n\n\t\tawait fetch(UrlBuilder.routine.routineExercisesApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(routineExerciseToUpdate)\n\t\t})\n\t\t\t.then(response => response.json())\n\t\t\t.then(() => this.props.closeSelfFunc(true,\n\t\t\t\t<>\n\t\t\t\t\t<>{this.state.exercise.name + ' has been moved to '}>\n\t\t\t\t\t {routine.name}\n\t\t\t\t\t<>{'.'}>\n\t\t\t\t>\n\t\t\t));\n\t}\n\n\tcloseDropDownCommentAnchorEl = () => {\n\t\tthis.setState({\n\t\t\thelpPopupAnchorEl: null\n\t\t});\n\t};\n\n\topenHelpPopupAnchorEl = (event) => {\n\t\tthis.setState({\n\t\t\thelpPopupAnchorEl: event.currentTarget\n\t\t});\n\t};\n\t\n\trenderHelpPopup = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn \n\t\t\t\n\t\t\t\tMoves the exercise to another routine, along with all its data: logs, goals, and statistics. Your historical workouts will be updated to reflect this change.\n\t\t\t \n\t\t \n\t}\n\n\trenderLoadingDialog = () => {\n\t\treturn (\n \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t \n\t\t\t
\n );\n\t}\n\n\trenderRoutines = () => {\n\t\treturn \n\t\t\t{this.state.routines.map((routine) => (\n\t\t\t\t this.moveExerciseToRoutine(routine)} key={routine.id} disabled={routine.id === this.state.routine.id}>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{routine.name.charAt(0).toUpperCase()}\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t))}\n\t\t
;\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tlet content;\n\t\tif (this.hasNeverBeenFetched()) {\n\t\t\tcontent = this.renderLoadingDialog();\n\t\t} else {\n\t\t\tcontent = this.renderRoutines();\n\t\t}\n\t\t\n\t\treturn (\n this.props.closeSelfFunc(false)} open={true} TransitionComponent={Transition}>\n \n \n \n Move {this.state.exercise.name} to:\n \n\n \n\n
\n \n \n \n
\n \n \n \n \n {this.renderHelpPopup()}\n\n {content}\n \n );\n\t}\n}\n\nexport default withStyles(useStyles)(MoveRoutineExerciseDialog);","import React, {Component} from 'react';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Box from \"@mui/material/Box\";\nimport withStyles from '@mui/styles/withStyles';\nimport Grid from \"@mui/material/Grid\";\nimport RoutineExerciseDetailCard from \"../partials/RoutineExerciseDetailCard\";\nimport {PageDataContext} from \"../../../core/PageDataContext\";\nimport Fab from \"@mui/material/Fab\";\nimport WorkoutCallToAction from \"../../workout/WorkoutCallToAction\";\nimport Divider from \"@mui/material/Divider\";\nimport Snackbar from \"@mui/material/Snackbar\";\nimport Button from \"@mui/material/Button\";\nimport Skeleton from '@mui/material/Skeleton';\nimport Fade from \"@mui/material/Fade\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport {UserLocalStorage} from \"../../../core/storage/UserLocalStorage\";\nimport IconButton from \"@mui/material/IconButton\";\nimport AddToPhotosIcon from \"@mui/icons-material/AddToPhotos\";\nimport Typography from \"@mui/material/Typography\";\nimport RoutineExerciseDetailBloop from \"../partials/RoutineExerciseDetailBloop\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport moment from \"moment\";\nimport {DragDropContext, Draggable, Droppable} from \"react-beautiful-dnd\";\nimport RoutineExercisesEditFormBloop from \"../partials/RoutineExercisesEditFormBloop\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport RoutineSupersetMenuPopUp from \"../partials/RoutineSupersetMenuPopUp\";\nimport MoveRoutineExerciseDialog from \"../partials/MoveRoutineExerciseDialog\";\nimport UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport Slide from \"@mui/material/Slide\";\nimport ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport SpeedDial from \"@mui/material/SpeedDial\";\nimport MenuIcon from \"@mui/icons-material/Menu\";\nimport SpeedDialAction from \"@mui/material/SpeedDialAction\";\nimport ArchiveIcon from \"@mui/icons-material/Archive\";\n\nconst useStyles = theme => ({\n\tspeedDialWrapper: {\n\t\tmargin: 0,\n\t\ttop: 'auto',\n\t\tright: 35,\n\t\tbottom: 80,\n\t\tleft: 'auto',\n\t\tposition: 'fixed',\n\t},\n\troot: {\n\t\twidth: '100%',\n\t},\n\tsnackbar: {\n\t\tbottom: 75,\n\t\t[theme.breakpoints.down('sm')]: {\n\t\t\tbottom: 150,\n\t\t},\n\t},\n\tlargeIcon: {\n\t\twidth: 200,\n\t\theight: 200,\n\t},\n\tmedia: {\n\t\theight: 125,\n\t},\n\tmediaSmaller: {\n\t\theight: 65,\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nclass RoutineDetail extends Component {\n\t/**\n\t * @param props No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The routine we want to show details for.\n\t\t\t */\n\t\t\troutine: UserLocalStorage.get(this.getApiUrl()) ?? null,\n\t\t\t/**\n\t\t\t * Used as a component 'is loading' indicator.\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Is the popup to add a new exercise to this routine open?\n\t\t\t */\n\t\t\tisEditRoutineBloopOpen: false,\n\t\t\t/**\n\t\t\t * The toast notification if it should show up. Null if it shouldn't show. Initializing\n\t\t\t * this will cause the toast notification to show up.\n\t\t\t */\n\t\t\ttoastNotificationObject: null,\n\t\t\t/**\n\t\t\t * The exercise ID we should open the bloop for. We use ID and not exercise to make sure the exercise we were given is not outdated.\n\t\t\t * This class will always hold the most up-to-date exercise.\n\t\t\t */\n\t\t\troutineExerciseIdBloopOpen: null,\n\t\t\t/**\n\t\t\t * If we want to delete a routine-exercise, then we first confirm with a dialog.\n\t\t\t */\n\t\t\troutineExerciseToConfirmDeletion: null,\n\t\t\t/**\n\t\t\t * The routine exercise to move.\n\t\t\t */\n\t\t\texerciseToMove: null,\n\t\t\t/**\n\t\t\t * The exercise to add a superset for.\n\t\t\t */\n\t\t\tsupersetMenuExercise: null,\n\t\t\t/**\n\t\t\t * Speed dial open?\n\t\t\t */\n\t\t\tisSpeedDialOpen: false,\n\t\t\t/**\n\t\t\t * Are we going into archived mode?\n\t\t\t */\n\t\t\tisInArchivedMode: false,\n\t\t};\n\t}\n\t\n\tgetApiUrl = (datetime = null) => {\n\t\treturn UrlBuilder.routine.routinesApi(\n\t\t\tthis.props.params.routineId, \n\t\t\tfalse,\n\t\t\tthis.state?.isInArchivedMode ?? false,\n\t\t\tdatetime\n\t\t);\n\t}\n\n\tcomponentDidMount() {\n\t\tif (this.hasNeverBeenFetched()) {\n\t\t\tthis.fetchRoutine().then(() => {\n\t\t\t\tthis.openBloopFromURLIfPossible();\n\t\t\t});\n\t\t} else {\n\t\t\tthis.openBloopFromURLIfPossible();\n\t\t\tthis.fetchRoutine(); // Don't try to open bloop after this.\n\t\t}\n\t}\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\tif (prevState.isInArchivedMode !== this.state.isInArchivedMode) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\t\n\tgetExerciseFromRoutineExerciseId = (routineExerciseId) => {\n\t\tif (routineExerciseId) {\n\t\t\tfor (const exercise of this.state.routine.exercises) {\n\t\t\t\tif (exercise.routineExerciseId == routineExerciseId) {\n\t\t\t\t\treturn exercise;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\t\n\topenBloopFromURLIfPossible = () => {\n\t\tlet routineExerciseId = (new URLSearchParams(this.props.location.search)).get('exercise');\n\t\tif (routineExerciseId && this.state.routine && this.state.routine.exercises) {\n\t\t\tfor (const exercise of this.state.routine.exercises) {\n\t\t\t\tif (exercise.routineExerciseId == routineExerciseId) {\n\t\t\t\t\tthis.setState({\n\t\t\t\t\t\troutineExerciseIdBloopOpen: exercise.routineExerciseId,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfetchRoutine = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tlet datetime = (moment()).subtract(3, 'months');\n\n\t\tawait fetch(this.getApiUrl(datetime.format('L')))\n\t\t\t.then(response => response.json())\n\t\t\t.then(routine => {\n\t\t\t\tthis.context.setPageData({\n\t\t\t\t\ttitle: routine.isArchived ? \n\t\t\t\t\t\t(routine.name + ' - Archived' + (this.state?.isInArchivedMode ? ' Exercises' : '')) \n\t\t\t\t\t\t: \n\t\t\t\t\t\t(routine.name + (this.state?.isInArchivedMode ? ' - Archived Exercises' : '')),\n\t\t\t\t});\n\t\t\t\t\n\t\t\t\tthis.setState({\n\t\t\t\t\troutine: routine,\n\t\t\t\t\tisLoading: false\n\t\t\t\t});\n\t\t\t\t\n\t\t\t\tUserLocalStorage.set(this.getApiUrl(), routine);\n\t\t\t});\n\t}\n\n\trefresh = () => {\n\t\tthis.fetchRoutine();\n\t}\n\n\thandleBloopOpen = (exercise) => {\n\t\tif (exercise) {\n\t\t\tthis.setState({\n\t\t\t\troutineExerciseIdBloopOpen: exercise.routineExerciseId,\n\t\t\t});\n\n\t\t\tlet currentUrlParams = new URLSearchParams(window.location.search);\n\t\t\tcurrentUrlParams.set('exercise', exercise.routineExerciseId);\n\t\t\tthis.props.navigate(window.location.pathname + \"?\" + currentUrlParams.toString());\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\troutineExerciseIdBloopOpen: null,\n\t\t\t});\n\n\t\t\tthis.props.navigate(window.location.pathname);\n\t\t}\n\t};\n\n\tswitchExerciseBloop = (exercise) => {\n\t\tthis.setState({\n\t\t\troutineExerciseIdBloopOpen: null,\n\t\t});\n\t\t\n\t\t// Wait before doing this, to get the full animation :)\n\t\tsetTimeout(() => this.handleBloopOpen(exercise), 200);\n\t}\n\n\tupdateRoutineExercise = (newExercise) => {\n\t\tfor (let i = 0; i < this.state.routine.exercises.length; i++) {\n\t\t\tlet exercise = this.state.routine.exercises[i];\n\n\t\t\tif (exercise.routineExerciseId == newExercise.routineExerciseId) {\n\t\t\t\tlet routine = this.state.routine;\n\t\t\t\troutine.exercises[i] = newExercise;\n\t\t\t\tthis.setState({\n\t\t\t\t\troutine: routine,\n\t\t\t\t});\n\t\t\t\tUserLocalStorage.set(UrlBuilder.routine.routinesApi(this.props.params.routineId), routine);\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\ttoggleEditRoutineBloop = () => {\n\t\tthis.setState({\n\t\t\tisEditRoutineBloopOpen: !this.state.isEditRoutineBloopOpen,\n\t\t});\n\t}\n\n\trefreshAfterEdit = (shouldRefresh = true) => {\n\t\tthis.setState({\n\t\t\tisEditRoutineBloopOpen: false,\n\t\t});\n\t\t\n\t\tif (shouldRefresh) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\n\tdeleteRoutineExercise = async (exercise) => {\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t\troutineExerciseToConfirmDeletion: null\n\t\t});\n\t\t\n\t\tawait fetch(UrlBuilder.routine.routineExercisesApi(exercise.routineExerciseId), {\n\t\t\tmethod: 'DELETE',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t})\n\t\t\t.then(this.refresh)\n\n\t\tthis.showRoutineExerciseDeletedToastNotification(exercise);\n\t}\n\n\tshowRoutineExerciseDeletedToastNotification = (exercise) => {\n\t\tconst {classes} = this.props\n\t\tthis.setState({\n\t\t\ttoastNotificationObject:
\n\t\t});\n\t}\n\n\tshowRoutineExerciseMovedToastNotification = (message) => {\n\t\tconst {classes} = this.props\n\t\tthis.setState({\n\t\t\ttoastNotificationObject:
\n\t\t});\n\t}\n\n\tshowExerciseArchiveToastNotification = (exercise, mode) => {\n\t\tconst {classes} = this.props\n\n\t\tlet toastNotificationObject;\n\t\tif (mode === 'archive_routine') {\n\t\t\ttoastNotificationObject =
this.archiveRoutineExercise(exercise, false)}>\n\t\t\t\t\t\tUndo\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t/>;\n\t\t} else if (mode === 'unarchive_routine') {\n\t\t\ttoastNotificationObject = ;\n\t\t}\n\n\t\tthis.setState({\n\t\t\ttoastNotificationObject: toastNotificationObject\n\t\t});\n\t}\n\t\n\tcloseToastNotification = (e, reason) => {\n\t\tthis.setState({\n\t\t\ttoastNotificationObject: null\n\t\t});\n\t}\n\n\tarchiveRoutineExercise = async (exercise, shouldArchive) => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tlet routineExerciseView = {\n\t\t\tid: exercise.routineExerciseId,\n\t\t\tisArchived: shouldArchive\n\t\t};\n\n\t\tawait fetch(UrlBuilder.routine.routineExercisesApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'accept': 'application/json',\n\t\t\t\t'content-type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(routineExerciseView)\n\t\t})\n\t\t\t.then(this.refresh);\n\n\t\t// Refresh will set isLoading to false if success.\n\n\t\tlet mode = shouldArchive ? 'archive_routine' : 'unarchive_routine';\n\t\tthis.showExerciseArchiveToastNotification(exercise, mode)\n\t}\n\n\trenderEmptyExercises = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t\n\n\t\t\t\n\t\t\t\t{this.state.isInArchivedMode ?\n\t\t\t\t\t this.toggleArchivedMode(false)}\n\t\t\t\t\t\tsize=\"large\">\n\t\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t:\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t}\n\t\t\t \n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{this.state.isInArchivedMode ?\n\t\t\t\t\t\t\"You have no archived exercises\"\n\t\t\t\t\t\t: \"No exercises in routine\"\n\t\t\t\t\t}\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{this.state.isInArchivedMode ?\n\t\t\t\t\t\t this.toggleArchivedMode(false)}>\n\t\t\t\t\t\t\tGo back \n\t\t\t\t\t\t \n\t\t\t\t\t\t:\n\t\t\t\t\t\t\n\t\t\t\t\t\t\tAdd an exercise \n\t\t\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\n\t reorder = (list, startIndex, endIndex) => {\n\t\tconst result = Array.from(list);\n\t\tconst [removed] = result.splice(startIndex, 1);\n\t\tresult.splice(endIndex, 0, removed);\n\n\t\treturn result;\n\t};\n\n\tonDragEnd = async (result) => {\n\t\t// dropped outside the list\n\t\tif (!result.destination) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tlet routine = this.state.routine;\n\n\t\troutine.exercises = this.reorder(\n\t\t\troutine.exercises,\n\t\t\tresult.source.index,\n\t\t\tresult.destination.index\n\t\t);\n\n\t\tthis.setState({\n\t\t\troutine: routine\n\t\t});\n\t\tthis.forceUpdate();\n\t\t\n\t\tawait this.submitRoutineExerciseReordering(routine.exercises);\n\t};\n\t\n\tsubmitRoutineExerciseReordering = async (exercises) => {\n\t\tlet exerciseViews = [];\n\t\texercises.forEach(exercise => exerciseViews.push({\n\t\t\troutineExerciseId: exercise.routineExerciseId\n\t\t}));\n\t\t\n\t\tlet routine = {\n\t\t\tId: this.state.routine.id,\n\t\t\texercises: exerciseViews,\n\t\t}\n\t\t\n\t\tawait fetch(UrlBuilder.routine.routinesApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(routine)\n\t\t});\n\t\t\n\t\tUserLocalStorage.remove(this.getApiUrl()); // Let's stale the localStorage so that the next time a refresh occurs it doesnt look choppy.\n\t}\n\t\n\topenSupersetMenu = (exercise) => {\n\t\tthis.setState({\n\t\t\tsupersetMenuExercise: exercise,\n\t\t});\n\n\t\tif (exercise === null) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\n\trenderExerciseCards = () => {\n\t\treturn <>\n\t\t\t{!this.state.routine.isArchived && !this.state.isInArchivedMode ? : null}\n\n\t\t\t \n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{(provided, snapshot) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.routine.exercises.map((exercise, index) => {\n\t\t\t\t\t\t\t\tlet key = \"exercise_\" + exercise.routineExerciseId;\n\t\t\t\t\t\t\t\treturn \n\t\t\t\t\t\t\t\t\t{(provided, snapshot) => (\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t this.archiveRoutineExercise(exercise, shouldArchive)}\n\t\t\t\t\t\t\t\t\t\t\t\tsetBloopOpen={this.handleBloopOpen}\n\t\t\t\t\t\t\t\t\t\t\t\tisInArchivedMode={this.state.isInArchivedMode}\n\t\t\t\t\t\t\t\t\t\t\t\topenSupersetMenu={this.openSupersetMenu}\n\t\t\t\t\t\t\t\t\t\t\t\tdragHandle={ }\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t \n\t\t\t\t\t)}\n\t\t\t\t \n\t\t\t \n\n\t\t\t this.handleBloopOpen(null)}\n\t\t\t\tupdateParentExercise={this.updateRoutineExercise}\n\t\t\t\tswitchExerciseBloop={this.switchExerciseBloop}\n\t\t\t/>\n\t\t\t\n\t\t\t this.openSupersetMenu(null)}\n\t\t\t/>\n\t\t>;\n\t}\n\t\n\trenderPage = () => {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{!this.state.routine.isArchived ?\n\t\t\t\t\t : null\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t{this.state.routine.exercises && this.state.routine.exercises.length > 0 ?\n\t\t\t\t\tthis.renderExerciseCards()\n\t\t\t\t\t: this.renderEmptyExercises()\n\t\t\t\t}\n\t\t\t>\n\t\t);\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.routine === null;\n\t}\n\n\trenderLoadingSkeletons = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\n\trenderRoutineExerciseDeleteConfirmationDialog = () => {\n\t\tif (!this.state.routineExerciseToConfirmDeletion) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.openRoutineExerciseDeleteConfirmationDialog(null)}\n\t\t\tTransitionComponent={Transition}\n\t\t>\n\t\t\t{\"Are you sure you want to delete \" + this.state.routineExerciseToConfirmDeletion.name + '?'} \n\t\t\t\n\t\t\t\t\n\t\t\t\t\tThis will delete this routine-exercise combination and its logs (including your historical workout logs for this specific routine-exercise). Other logs for this exercise will not be affected.\n\t\t\t\t\t \n\t\t\t\t\tThis cannot be undone.\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t this.openRoutineExerciseDeleteConfirmationDialog(null)}>\n\t\t\t\t\tCancel\n\t\t\t\t \n\t\t\t\t this.deleteRoutineExercise(this.state.routineExerciseToConfirmDeletion)} color=\"primary\" autoFocus>\n\t\t\t\t\tDelete\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\topenRoutineExerciseDeleteConfirmationDialog = (routineExercise) => {\n\t\tthis.setState({\n\t\t\troutineExerciseToConfirmDeletion: routineExercise,\n\t\t});\n\t}\n\n\trenderMoveRoutineExerciseDeleteConfirmationDialog = () => {\n\t\tif (!this.state.exerciseToMove) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.openMoveRoutineExerciseDialog(null, shouldRefresh, message)}\n\t\t/>\n\t}\n\t\n\topenMoveRoutineExerciseDialog = (exercise, shouldRefresh = false, message= null) => {\n\t\tthis.setState({\n\t\t\texerciseToMove: exercise,\n\t\t});\n\n\t\tif (shouldRefresh) {\n\t\t\tthis.refresh();\n\t\t\tif (message) { // Have the previous one at hand to show a toast notification.\n\t\t\t\tthis.showRoutineExerciseMovedToastNotification(message);\n\t\t\t}\n\t\t}\n\t}\n\n\topenSpeedDial = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisSpeedDialOpen: isOpen,\n\t\t})\n\t}\n\n\ttoggleArchivedMode = (isInArchiveMode) => {\n\t\tthis.setState({\n\t\t\tisInArchivedMode: isInArchiveMode,\n\t\t\tisSpeedDialOpen: false,\n\t\t\troutine: null // will fetch again, so set routines null\n\t\t});\n\t}\n\n\trenderSpeedDial = () => {\n\t\tconst {classes} = this.props\n\n\t\tif (this.state.isInArchivedMode) {\n\t\t\treturn this.toggleArchivedMode(false)}>\n\t\t\t\t \n\t\t\t \n\t\t}\n\n\t\treturn }\n\t\t\tonClose={() => this.openSpeedDial(false)}\n\t\t\tonOpen={() => this.openSpeedDial(true)}\n\t\t\tdisabled={!this.state.routine || this.state.routine.isArchived}\n\t\t\topen={this.state.isSpeedDialOpen}\n\t\t\tariaLabel={'Routine Menu'}\n\t\t>\n\t\t\t }\n\t\t\t\ttooltipTitle={Add Exercise }\n\t\t\t\ttooltipOpen\n\t\t\t\tonClick={this.toggleEditRoutineBloop}\n\t\t\t\ttitle={'Add Exercise'}\n\t\t\t/>\n\t\t\t }\n\t\t\t\ttooltipTitle={Archived }\n\t\t\t\ttooltipOpen\n\t\t\t\tonClick={() => this.toggleArchivedMode(true)}\n\t\t\t\ttitle={'Archived'}\n\t\t\t/>\n\t\t;\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t{this.renderRoutineExerciseDeleteConfirmationDialog()}\n\n\t\t\t\t{this.renderMoveRoutineExerciseDeleteConfirmationDialog()}\n\n\t\t\t\t{\n\t\t\t\t\tthis.hasNeverBeenFetched() ?\n\t\t\t\t\t\tthis.renderLoadingSkeletons()\n\t\t\t\t\t\t: this.renderPage()\n\t\t\t\t}\n\n\t\t\t\t{this.renderSpeedDial()}\n\n\t\t\t\t{this.state.toastNotificationObject}\n\t\t\t
\n\t\t);\n\t}\n}\n\nRoutineDetail.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(RoutineDetail));","import { createTheme } from '@mui/material';\nimport {grey} from \"@mui/material/colors\";\n\nconst theme = createTheme({\n\tpalette: {\n\t\tprimary: {\n\t\t\tmain: '#2d3b4a',\n\t\t},\n\t\tsecondary: {\n\t\t\tmain: '#f50057',\n\t\t\tstopWorkoutColor: '#ffcdd2',\n\t\t\tstartWorkoutColor: '#c8e6c9',\n\t\t},\n\t\tcontrastThreshold: 2,\n\t\ttonalOffset: 0.2,\n\t},\n\tcomponents: {\n\t\tMuiButton: {\n\t\t\tvariants: [\n\t\t\t\t{\n\t\t\t\t\tprops: {variant: \"contained\", color: \"grey\"},\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tbackgroundColor: grey[300],\n\t\t\t\t\t\tcolor: '#242424',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t},\n\t\tMuiLink: {\n\t\t\tdefaultProps: {\n\t\t\t\tunderline: 'none',\n\t\t\t},\n\t\t},\n\t},\n});\n\nexport default theme;","import { createTheme } from '@mui/material';\nimport {grey} from \"@mui/material/colors\";\n\nconst workoutTheme = createTheme({\n\tpalette: {\n\t\tprimary: {\n\t\t\tmain: '#642c2c',\n\t\t},\n\t\tsecondary: {\n\t\t\tmain: '#e54b4b',\n\t\t\tstopWorkoutColor: '#ffcdd2',\n\t\t\tstartWorkoutColor: '#c8e6c9',\n\t\t},\n\t\tcontrastThreshold: 2,\n\t\ttonalOffset: 0.2,\n\t},\n\tcomponents: {\n\t\tMuiButton: {\n\t\t\tvariants: [\n\t\t\t\t{\n\t\t\t\t\tprops: {variant: \"contained\", color: \"grey\"},\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tbackgroundColor: grey[300],\n\t\t\t\t\t\tcolor: '#242424',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t},\n\t\tMuiLink: {\n\t\t\tdefaultProps: {\n\t\t\t\tunderline: 'none',\n\t\t\t},\n\t\t},\n\t},\n});\n\nexport default workoutTheme;","import { createTheme } from '@mui/material';\nimport {grey} from \"@mui/material/colors\";\n\nconst theme = createTheme({\n\tpalette: {\n\t\tmode: 'dark',\n\t\tprimary: {\n\t\t\tmain: '#2d3b4a',\n\t\t\tdark: 'rgb(54, 73, 91)',\n\t\t},\n\t\tsecondary: {\n\t\t\tmain: '#f50057',\n\t\t\tstopWorkoutColor: '#580505',\n\t\t\tstartWorkoutColor: '#0f3b04',\n\t\t},\n\t\ttext: {\n\t\t\tprimary: '#f6f6f6',\n\t\t\tsecondary: '#d9d9d9',\n\t\t},\n\t\tbackground: {\n\t\t\tpaper: '#1f2a33',\n\t\t\tdefault: '#151b24',\n\t\t},\n\t\tdivider: '#2c3340',\n\t},\n\tcomponents: {\n\t\tMuiButton: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {\n\t\t\t\t\tcolor: '#f6f6f6',\n\t\t\t\t},\n\t\t\t},\n\t\t\tvariants: [\n\t\t\t\t{\n\t\t\t\t\tprops: {variant: \"contained\", color: \"grey\"},\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tbackgroundColor: grey[300],\n\t\t\t\t\t\tcolor: '#242424',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t]\n\t\t},\n\t\tMuiPaper: {\n\t\t\tstyleOverrides: { \n\t\t\t\troot: { \n\t\t\t\t\tbackgroundImage: 'unset', // https://mui.com/guides/migration-v4/#paper\n\t\t\t\t} \n\t\t\t},\n\t\t},\n\t\tMuiLink: {\n\t\t\tdefaultProps: {\n\t\t\t\tunderline: 'none', // https://mui.com/guides/migration-v4/#link-underline-hover\n\t\t\t},\n\t\t},\n\t\tMuiDivider: {\n\t\t\tstyleOverrides: {\n\t\t\t\tlight: {\n\t\t\t\t\tborderColor: 'rgb(36,42,53)'\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t},\n});\n\nexport default theme;","import React, {PureComponent} from \"react\";\nimport withStyles from '@mui/styles/withStyles';\nimport moment from \"moment\";\nimport Button from \"@mui/material/Button\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Dialog from \"@mui/material/Dialog\";\nimport Slide from \"@mui/material/Slide\";\nimport Grid from \"@mui/material/Grid\";\nimport {DialogTitle, Stack} from \"@mui/material\";\nimport SentimentSatisfiedIcon from '@mui/icons-material/SentimentSatisfied';\nimport {SentimentVeryDissatisfied, SentimentVerySatisfied} from \"@mui/icons-material\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport AdapterDateFns from \"@mui/lab/AdapterDateFns\";\nimport {LocalizationProvider, TimePicker} from \"@mui/lab\";\nimport TextField from \"@mui/material/TextField\";\n\nconst useStyles = theme => ({\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\ttitle: {\n\t\tmarginLeft: theme.spacing(2),\n\t\tflex: 100,\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tnested: {\n\t\tpaddingLeft: theme.spacing(5),\n\t},\n\tmedia: {\n\t\theight: 125,\n\t},\n\tfloatRight: {\n\t\tfloat: 'right',\n\t},\n\tlogComment: {\n\t\tpadding: theme.spacing(2),\n\t},\n\tcontainer: {\n\t\tbackgroundColor: theme.palette.background.default,\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\nclass WorkoutCompleteEditForm extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'isOpen': Is it open? is it safe?!\n\t * 'setUser': Updates the user -> needed to finish the workout.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The workout.\n\t\t\t */\n\t\t\tworkout: props.workout,\n\t\t\t/**\n\t\t\t * Is it open?\n\t\t\t */\n\t\t\tisOpen: false,\n\t\t\t/**\n\t\t\t * Is it loading?\n\t\t\t */\n\t\t\tisSubmissionLoading: false,\n\t\t\t/**\n\t\t\t * The duration of the workout (a DateTime is defined if the isConfirmationDialogOpen is true)\n\t\t\t */\n\t\t\tworkoutDuration: null,\n\t\t\t/**\n\t\t\t * Which moodLevel?\n\t\t\t */\n\t\t\tmoodLevel: props.workout.moodLevel,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (nextProps.isOpen) {\n\t\t\tthis.setState({\n\t\t\t\tworkout: nextProps.workout,\n\t\t\t\tisOpen: true,\n\t\t\t\tmoodLevel: nextProps.workout.moodLevel,\n\t\t\t\tworkoutDuration: this.getDefaultWorkoutDurationDateTime(), // If the dialog is opened again, reset this date.\n\t\t\t\tisSubmissionLoading: false,\n\t\t\t});\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tisOpen: false,\n\t\t\t});\n\t\t}\n\t}\n\t\n\t/**\n\t * @param val A Datetime object indicating the duration (only HH:mm matters).\n\t */\n\tupdateWorkoutDuration = (val) => {\n\t\tthis.setState({\n\t\t\tworkoutDuration: val,\n\t\t});\n\t}\n\n\tsubmitWorkout = async (event, moodLevel = null, workoutEndedMoment = null) => {\n\t\tevent.preventDefault();\n\n\t\tif (this.isWorkoutAlreadyFinished()) {\n\t\t\tawait this.submitAsEdit(moodLevel, workoutEndedMoment);\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tawait this.submitAsNew(moodLevel, workoutEndedMoment);\n\t}\n\t\n\tsubmitAsEdit = async (moodLevel = null, workoutEndedMoment = null) => {\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet workout = {\n\t\t\tid: this.state.workout.id,\n\t\t\tdateTimeEnded: workoutEndedMoment ? workoutEndedMoment.format('L, h:mm:ss A') : moment().format('L, h:mm:ss A'),\n\t\t};\n\n\t\tif (moodLevel) {\n\t\t\tworkout['moodLevel'] = moodLevel;\n\t\t}\n\n\t\tawait fetch(UrlBuilder.workout.workoutsApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(workout)\n\t\t})\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.props.closeSelfFunc(true);\n\t\t\t\t\n\t\t\t\tthis.setState({\n\t\t\t\t\tisSubmissionLoading: false,\n\t\t\t\t});\n\t\t\t});\n\t}\n\n\tsubmitAsNew = async (moodLevel = null, workoutEndedMoment = null) => {\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet workout = {\n\t\t\tdateTimeEnded: workoutEndedMoment ? workoutEndedMoment.format('L, h:mm:ss A') : moment().format('L, h:mm:ss A'),\n\t\t\tcomment: this.state.workoutComment,\n\t\t};\n\n\t\tif (moodLevel) {\n\t\t\tworkout['moodLevel'] = moodLevel;\n\t\t}\n\n\t\tawait fetch(UrlBuilder.workout.finishWorkoutApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(workout)\n\t\t})\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.props.closeSelfFunc(true);\n\n\t\t\t\tlet user = response.data;\n\n\t\t\t\tthis.props.setUser(user);\n\n\t\t\t\tif (this.state.workout.routines.length > 0) { // Only redirect if you had some exercises to show.. (backend logic duplication)\n\t\t\t\t\tthis.props.navigate(UrlBuilder.workout.workoutDetailPage(this.state.workout.id, true));\n\t\t\t\t}\n\t\t\t})\n\t}\n\n\t/**\n\t * Based on the time started, it gets the time ended based on the workoutDuration DateTime var.\n\t * \n\t * @returns {Moment}\n\t */\n\tgetMomentEnded = (workoutDurationDateTime) => {\n\t\tconst dateTimeStarted = moment(this.state.workout.dateTimeStarted);\n\n\t\treturn dateTimeStarted\n\t\t\t.add(workoutDurationDateTime.getHours(), 'hours')\n\t\t\t.add(workoutDurationDateTime.getMinutes(), 'minutes');\n\t}\n\n\t/**\n\t * Gets the default workout duration dateTime (based on the dateTimeStarted and now()).\n\t * \n\t * @returns {Date}\n\t */\n\tgetDefaultWorkoutDurationDateTime = () => {\n\t\tif (this.state.workout.isPastWorkout && !this.isWorkoutAlreadyFinished()) { // If it's a past workout in progress, we don't get a default time, we let the user decide it.\n\t\t\treturn null;\n\t\t}\n\t\t\n\t\tconst dateTimeStarted = moment(this.state.workout.dateTimeStarted);\n\t\t\n\t\tconst dateTimeEnded = this.isWorkoutAlreadyFinished() ? moment(this.state.workout.dateTimeEnded) : moment();\n\t\t\n\t\tlet workoutDurationObj = moment.duration(dateTimeEnded.diff(dateTimeStarted));\n\t\tlet workoutDuration = moment.utc(workoutDurationObj.asMilliseconds());\n\t\t\n\t\t// The duration is any date but with a specific time.\n\t\tconst duration = new Date();\n\t\tduration.setHours(workoutDuration.hours());\n\t\tduration.setMinutes(workoutDuration.minutes());\n\t\tduration.setMilliseconds(workoutDuration.milliseconds());\n\t\t\n\t\treturn duration;\n\t}\n\n\tisWorkoutAlreadyFinished = () => {\n\t\treturn !!this.state.workout.dateTimeEnded;\n\t}\n\n\topenConfirmationDialog = (isOpen) => {\n\t\tif (isOpen) {\n\t\t\tthis.setState({\n\t\t\t\tisConfirmationDialogOpen: true,\n\t\t\t\t\n\t\t\t});\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tisConfirmationDialogOpen: false,\n\t\t\t});\n\t\t}\n\t}\n\t\n\tsetMoodLevel = (moodLevel) => {\n\t\tthis.setState({\n\t\t\tmoodLevel: this.state.moodLevel === moodLevel ? null : moodLevel, // If equal to now, remove it.\n\t\t});\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tconst dateTimeStarted = moment(this.state.workout.dateTimeStarted);\n\t\tconst momentEnded = this.state.workoutDuration ? this.getMomentEnded(this.state.workoutDuration) : null;\n\t\t\n\t\treturn (\n this.props.closeSelfFunc(false)} TransitionComponent={Transition}>\n \n {this.isWorkoutAlreadyFinished() ?\n 'Edit Workout'\n :\n this.state.workout.isPastWorkout ? 'Complete Past Workout' : 'Complete Workout'\n }\n \n \n \n \n \n \n \n \n Started at:\n \n \n \n\t \n\t\t \n\t\t\t \n\t\t\t\t {dateTimeStarted.format('LT')}\n\t\t\t \n\t\t \n\t\t \n\t\t\t \n\t\t\t\t {dateTimeStarted.format('DD/MM/YYYY')}\n\t\t\t \n\t\t \n\t \n \n \n \n\n\t \n\t\t \n\t\t\t \n\t\t\t\t \n\t\t\t\t\t Select workout duration:\n\t\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t\t \n\t\t\t\t\t }\n\t\t\t\t\t />\n\t\t\t\t \n\t\t\t \n\t\t \n\t \n \n \n \n \n \n Ended at:\n \n \n \n\t \n\t\t \n\t\t\t \n\t\t\t\t {momentEnded ? momentEnded.format('LT') : '-'}\n\t\t\t \n\t\t \n\t\t {momentEnded &&\n\t\t\t \n\t\t\t\t \n\t\t\t\t\t {momentEnded.format('DD/MM/YYYY')}\n\t\t\t\t \n\t\t\t \n\t\t }\n\t \n \n \n \n\n \n \n \n \n Rate your mood:\n \n \n \n \n \n this.setMoodLevel(1)}\n size=\"large\">\n \n \n \n \n this.setMoodLevel(2)}\n size=\"large\">\n \n \n \n \n this.setMoodLevel(3)}\n size=\"large\">\n \n \n \n \n \n \n \n \n \n \n this.props.closeSelfFunc(false)}>Close \n\n this.submitWorkout(e, this.state.moodLevel, this.getMomentEnded(this.state.workoutDuration))}\n >\n {this.state.isSubmissionLoading ?\n <> Loading... >\n :\n (this.isWorkoutAlreadyFinished() ?\n 'Save Workout' \n :\n <>{this.state.workout && this.state.workout.routines.length > 0 ? 'Submit Workout' : 'Quit Workout'}>\n\n )\n }\n \n \n \n );\n\t}\n}\n\t\nexport default withStyles(useStyles)(withRouter(WorkoutCompleteEditForm));","import React, {PureComponent} from 'react';\nimport { Line } from 'react-chartjs-2';\nimport {ColorPicker} from \"../../core/colors/ColorPicker\";\n\nexport default class LineGraph extends PureComponent {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\tdata: this.props.data,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\tdata: nextProps.data,\n\t\t});\n\t}\n\t\n\trender() {\n\t\tlet color = ColorPicker.pick(this.state.data.label);\n\t\t\n\t\tconst data = {\n\t\t\tlabels: this.state.data.x,\n\t\t\tdatasets: [\n\t\t\t\t{\n\t\t\t\t\tlabel: this.state.data.label,\n\t\t\t\t\tfill: false,\n\t\t\t\t\tlineTension: 0.1,\n\t\t\t\t\tbackgroundColor: color,\n\t\t\t\t\tborderColor: color,\n\t\t\t\t\tborderCapStyle: 'butt',\n\t\t\t\t\tborderDash: [],\n\t\t\t\t\tborderDashOffset: 0.0,\n\t\t\t\t\tborderJoinStyle: 'miter',\n\t\t\t\t\tpointBorderColor: color,\n\t\t\t\t\tpointBackgroundColor: '#fff',\n\t\t\t\t\tpointBorderWidth: 1,\n\t\t\t\t\tpointHoverRadius: 5,\n\t\t\t\t\tpointHoverBackgroundColor: color,\n\t\t\t\t\tpointHoverBorderColor: color,\n\t\t\t\t\tpointHoverBorderWidth: 2,\n\t\t\t\t\tpointRadius: 1,\n\t\t\t\t\tpointHitRadius: 10,\n\t\t\t\t\tdata: this.state.data.y\n\t\t\t\t}\n\t\t\t]\n\t\t};\n\t\t\n\t\tlet options= {\n\t\t\tlegend: {\n\t\t\t\tdisplay: typeof(this.props.showLegend) === 'boolean' ? this.props.showLegend : true,\n\t\t\t},\n\t\t};\n\n\t\treturn (\n\t\t\t\n\t\t);\n\t}\n\n\tcomponentDidMount() {\n\t\tconst { datasets } = this.refs.chart.chartInstance.data\n\t}\n}","export class WorkoutVolumeParser {\n\tstatic parse(workouts) {\n\t\tlet volumes = [];\n\t\tlet dates = [];\n\n\t\tfor (let i = 0; i < workouts.length; i++) {\n\t\t\tlet workout = workouts[i];\n\t\t\t\n\t\t\tvolumes.push(workout.totalVolume);\n\t\t\tdates.push((new Date(workout.dateTimeStarted)).toLocaleDateString());\n\t\t}\n\t\t\n\t\treturn {\n\t\t\ty: volumes,\n\t\t\tx: dates,\n\t\t\tlabel: \"Total Volume\"\n\t\t};\n\t}\n}","import React, {PureComponent} from \"react\";\nimport withStyles from '@mui/styles/withStyles';\nimport {PageDataContext} from \"../../../core/PageDataContext\";\nimport Typography from \"@mui/material/Typography\";\nimport ListItem from \"@mui/material/ListItem\";\nimport ListItemIcon from \"@mui/material/ListItemIcon\";\nimport FitnessCenterIcon from \"@mui/icons-material/FitnessCenter\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport List from \"@mui/material/List\";\nimport {AttributeValuesPrettier} from \"../../../core/prettifier/AttributeValuesPrettier\";\nimport IconButton from \"@mui/material/IconButton\";\nimport MessageIcon from \"@mui/icons-material/Message\";\nimport moment from \"moment\";\nimport Box from \"@mui/material/Box\";\nimport Divider from \"@mui/material/Divider\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport Menu from \"@mui/material/Menu\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport LineGraph from \"../../graphs/LineGraph\";\nimport {WorkoutVolumeParser} from \"../../../core/parser/WorkoutVolumeParser\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Popover from \"@mui/material/Popover\";\nimport SortIcon from \"@mui/icons-material/Sort\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport Tabs from \"@mui/material/Tabs\";\nimport Tab from \"@mui/material/Tab\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport {Doughnut} from \"react-chartjs-2\";\nimport {ExerciseType} from \"../../../core/models/ExerciseType\";\nimport {ExerciseTypeAttribute} from \"../../../core/models/ExerciseTypeAttribute\";\nimport Button from \"@mui/material/Button\";\n\nconst useStyles = theme => ({\n\tlogComment: {\n\t\tpadding: theme.spacing(2),\n\t},\n\tfloatRight: {\n\t\tfloat: 'right',\n\t},\n\tbutton: {\n\t\tminWidth: '30px',\n\t\twidth: '30px',\n\t\theight: '30px',\n\t\tpadding: '0',\n\t},\n});\n\nfunction a11yProps(index) {\n\treturn {\n\t\tid: `full-width-tab-${index}`,\n\t};\n}\n\nclass WorkoutLogsSection extends PureComponent {\n\t/**\n\t * No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The workout in mind.\n\t\t\t */\n\t\t\tworkout: props.workout,\n\t\t\t/**\n\t\t\t * Whether the graphs are being fetched.\n\t\t\t */\n\t\t\tisGraphsLoading: false,\n\t\t\t/**\n\t\t\t * Fetch from.. when?\n\t\t\t */\n\t\t\tdateRangeFetched: '3_months',\n\t\t\t/**\n\t\t\t * The workouts done per routine.\n\t\t\t */\n\t\t\troutineWorkouts: props.cachedWorkoutsData ?? {},\n\t\t\t/**\n\t\t\t * The graph menu.\n\t\t\t */\n\t\t\tgraphsMenuAnchorEl: null,\n\t\t\t/**\n\t\t\t * A log comment to show in the dropDownCommentAnchorEl, if any.\n\t\t\t */\n\t\t\tlogComment: null,\n\t\t\t/**\n\t\t\t * The selected tab index. By default it is 0 (the logs).\n\t\t\t */\n\t\t\tselectedTabIndex: 0,\n\t\t};\n\t}\n\t\n\tcomponentDidMount() {\n\t\tif (this.props.shouldUpdate === undefined || this.props.shouldUpdate) {\n\t\t\tthis.fetchWorkoutData();\n\t\t}\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (!nextProps.shouldUpdate || this.state.workout === nextProps.workout) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tthis.setState({\n\t\t\tworkout: nextProps.workout,\n\t\t});\n\n\t\tthis.fetchWorkoutData(null, nextProps.workout);\n\t}\n\t\n\tcomponentWillUnmount() {\n\t\tif ('saveCachedWorkoutsData' in this.props) {\n\t\t\t// We only save this if we unmount so we don't lose the state.\n\t\t\tthis.props.saveCachedWorkoutsData(this.state.routineWorkouts);\n\t\t}\n\t}\n\n\tselectTabIndex = (e, idx) => {\n\t\tthis.setState({\n\t\t\tselectedTabIndex: idx,\n\t\t});\n\t}\n\n\topenGraphMenu = (event) => {\n\t\tthis.setState({\n\t\t\tgraphsMenuAnchorEl: event.currentTarget\n\t\t});\n\t}\n\n\tcloseGraphMenu = () => {\n\t\tthis.setState({\n\t\t\tgraphsMenuAnchorEl: null\n\t\t});\n\t}\n\n\topenDropDownCommentAnchorEl = (event, comment) => {\n\t\tevent.stopPropagation();\n\t\tthis.setState({\n\t\t\tdropDownCommentAnchorEl: event.currentTarget,\n\t\t\tlogComment: comment,\n\t\t});\n\t};\n\n\tcloseDropDownCommentAnchorEl = () => {\n\t\tthis.setState({\n\t\t\tdropDownCommentAnchorEl: null,\n\t\t\tlogComment: null,\n\t\t});\n\t};\n\n\t/**\n\t * Fetch workout-data from start to end. Start is a shortName (3_months, 6_months, etc). End should be a real datetime string, or null, in which case we fallback to the state-workout.\n\t */\n\tfetchWorkoutData = async(start = null, workout = null) => {\n\t\tworkout = workout ?? this.state.workout;\n\n\t\tif (workout?.routines.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet end = workout.dateTimeStarted;\n\n\t\tlet datetime = (moment(end)).subtract(3, 'months');\n\t\tlet dateRangeFetched = '3_months';\n\t\tif (start) {\n\t\t\tswitch (start) {\n\t\t\t\tcase '1_month':\n\t\t\t\t\tdatetime = (moment()).subtract(1, 'month');\n\t\t\t\t\tdateRangeFetched = '1_month';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '3_months':\n\t\t\t\t\tdatetime = (moment()).subtract(3, 'months');\n\t\t\t\t\tdateRangeFetched = '3_months';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '6_months':\n\t\t\t\t\tdatetime = (moment()).subtract(6, 'months');\n\t\t\t\t\tdateRangeFetched = '6_months';\n\t\t\t\t\tbreak;\n\t\t\t\tcase '1_year':\n\t\t\t\t\tdatetime = (moment()).subtract(1, 'year');\n\t\t\t\t\tdateRangeFetched = '1_year';\n\t\t\t\t\tbreak;\n\t\t\t\tcase 'all_time':\n\t\t\t\t\tdatetime = null;\n\t\t\t\t\tdateRangeFetched = 'all_time';\n\t\t\t}\n\t\t}\n\n\t\tthis.setState({\n\t\t\tisGraphsLoading: true,\n\t\t\tdateRangeFetched: dateRangeFetched,\n\t\t\tgraphsMenuAnchorEl: null,\n\t\t});\n\n\t\tlet existingRoutineWorkouts = {};\n\t\tfor (const routine of workout.routines) {\n\t\t\tconst routineWorkoutsResponse = await fetch(\n\t\t\t\tUrlBuilder.workout.workoutsForRoutineApi(\n\t\t\t\t\troutine.id, \n\t\t\t\t\tdatetime ? datetime.format('L') : null,\n\t\t\t\t\tworkout.dateTimeStarted\n\t\t\t\t)\n\t\t\t).then(res => res.json());\n\n\t\t\tconst routineWorkouts = routineWorkoutsResponse.data;\n\n\t\t\texistingRoutineWorkouts[routine.id] = {\n\t\t\t\tname: routine.name,\n\t\t\t\tworkouts: routineWorkouts,\n\t\t\t};\n\n\t\t\tthis.setState({\n\t\t\t\troutineWorkouts: existingRoutineWorkouts,\n\t\t\t});\n\t\t\tthis.forceUpdate();\n\t\t}\n\n\t\tthis.setState({\n\t\t\tisGraphsLoading: false,\n\t\t});\n\t}\n\t\n\trenderVolumeGraphs = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n\t\t\t<>\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t{this.state.isGraphsLoading ?\n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t: null\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t\t Filter graphs by: \n\t\t\t\t\t this.fetchWorkoutData('1_month')} selected={this.state.dateRangeFetched === '1_month'}> 1 month \n\t\t\t\t\t this.fetchWorkoutData('3_months')} selected={this.state.dateRangeFetched === '3_months'}> 3 months \n\t\t\t\t\t this.fetchWorkoutData('6_months')} selected={this.state.dateRangeFetched === '6_months'}> 6 months \n\t\t\t\t\t this.fetchWorkoutData('1_year')} selected={this.state.dateRangeFetched === '1_year'}> 1 year \n\t\t\t\t\t this.fetchWorkoutData('all_time')} selected={this.state.dateRangeFetched === 'all_time'}> All time \n\t\t\t\t \n\n\t\t\t\t\n\n\t\t\t\t{Object.keys(this.state.routineWorkouts).map((routineId) =>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.routineWorkouts[routineId]?.name}:\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t)}\n\t\t\t>\n\t\t)\n\t}\n\t\n\trenderMuscleBreakdown = () => {\n\t\tlet musclesWorkByExerciseType = {};\n\n\t\tthis.state.workout?.routines.forEach(routine => {\n\t\t\troutine.exercises.forEach(exercise => {\n\t\t\t\texercise.muscles.forEach(exerciseMuscle => {\n\t\t\t\t\tlet totalWorkForExercise = 0;\n\n\t\t\t\t\tfor (const log of exercise.logs) {\n\t\t\t\t\t\tswitch (exercise.type.id) {\n\t\t\t\t\t\t\tcase ExerciseType.WeightAndReps:\n\t\t\t\t\t\t\tcase ExerciseType.DistanceAndTime:\n\t\t\t\t\t\t\tcase ExerciseType.WeightAndTime:\n\t\t\t\t\t\t\t\ttotalWorkForExercise += log.multiplied * exerciseMuscle.percentage / 100; // Out of the total volume, we only set a given percentage for it.\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase ExerciseType.Reps:\n\t\t\t\t\t\t\tcase ExerciseType.Time:\n\t\t\t\t\t\t\t\ttotalWorkForExercise += log.values[0] * exerciseMuscle.percentage / 100; // Out of the total volume, we only set a given percentage for it.\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Needed to initialize the exercise type object cuz JS is dumb.\n\t\t\t\t\tmusclesWorkByExerciseType[exercise.type.name] = musclesWorkByExerciseType[exercise.type.name] ?? {};\n\t\t\t\t\t\n\t\t\t\t\tmusclesWorkByExerciseType[exercise.type.name][exerciseMuscle.muscleId] = {\n\t\t\t\t\t\tname: exerciseMuscle.muscle.name,\n\t\t\t\t\t\tworkTotal: totalWorkForExercise + \n\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\tmusclesWorkByExerciseType[exercise.type.name] && musclesWorkByExerciseType[exercise.type.name][exerciseMuscle.muscleId]\n\t\t\t\t\t\t\t\t? musclesWorkByExerciseType[exercise.type.name][exerciseMuscle.muscleId].workTotal\n\t\t\t\t\t\t\t\t: 0\n\t\t\t\t\t\t\t),\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t})\n\t\t});\n\n\t\tif (Object.keys(musclesWorkByExerciseType).length === 0) {\n\t\t\treturn \n\t\t\t\t\tNo muscles have been configured for the exercises in this workout. Do this through the exercises page.\n\t\t\t \n\t\t}\n\n\t\tlet volumeChartDataByExerciseType = {};\n\n\t\tObject.keys(musclesWorkByExerciseType).forEach(exerciseTypeName => {\n\t\t\tlet labels = [];\n\t\t\tlet backgroundColors = [];\n\t\t\tlet totalWorkPerMuscle = [];\n\n\t\t\tlet muscles = musclesWorkByExerciseType[exerciseTypeName];\n\t\t\tObject.keys(muscles).forEach(muscleId => {\n\t\t\t\tlet muscle = muscles[muscleId];\n\n\t\t\t\tlabels.push(muscle.name);\n\t\t\t\tbackgroundColors.push(ColorPicker.pick(muscle.name + '_ab'));\n\n\t\t\t\ttotalWorkPerMuscle.push(muscle.workTotal);\n\t\t\t});\n\t\t\t\n\t\t\tlet totalWork = totalWorkPerMuscle.reduce((a,b) => a + b, 0);\n\n\t\t\tvolumeChartDataByExerciseType[exerciseTypeName] = {\n\t\t\t\tlabels: labels,\n\t\t\t\tdatasets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: 'Work per Muscle',\n\t\t\t\t\t\tdata: totalWorkPerMuscle.map(muscleWork => Math.round(muscleWork / totalWork * 1000) / 10),\n\t\t\t\t\t\tbackgroundColor: backgroundColors,\n\t\t\t\t\t\tborderWidth: 0,\n\t\t\t\t\t\tborderColor: '#aeaeae'\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t});\n\n\t\tlet exerciseTypes = Object.keys(volumeChartDataByExerciseType);\n\t\t\n\t\treturn <>\n\t\t\t\n\t\t\t\tMuscle Work Breakdown\n\t\t\t \n\n\t\t\t \n\t\t\t\n\t\t\t{exerciseTypes.map(exerciseTypeName =>\n\t\t\t\t\n\t\t\t\t\t{exerciseTypes.length > 1 &&\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{exerciseTypeName}-based exercises\n\t\t\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t)}\n\t\t>;\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn this.state.workout.routines.length > 0 ?\n\t\t\t<>\n\t\t\t\t{this.state.logComment && this.state.dropDownCommentAnchorEl ?\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.logComment}\n\t\t\t\t\t\t \n\t\t\t\t\t : null\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t{this.state.workout.routines.map(routine =>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{routine.name}\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t{routine.exercises.map(exercise => {\n\t\t\t\t\t\t\tconst key = routine.id + '_' + exercise.id;\n\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{exercise.logs.map((log, idx) =>\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{idx + 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{AttributeValuesPrettier.prettifyAttributeValues(exercise.type, log)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\texercise.type.attributes.find(attr => attr.key === ExerciseTypeAttribute.Reps) && !isNaN(log.rpeValue) && !isNaN(log.rirValue) ?\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t @{log.rpeValue},{log.rirValue}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t : null\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{log.comment ?\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t this.openDropDownCommentAnchorEl(e, log.comment)} className={classes.floatRight}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t : null\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{moment(log.dateCreated).format('HH:mm')}\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t \n\t\t\t\t)}\n\n\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t\n\n\t\t\t\t{this.state.selectedTabIndex === 0 && this.renderVolumeGraphs()}\n\t\t\t\t{this.state.selectedTabIndex === 1 && this.renderMuscleBreakdown()}\n\t\t\t>\n\t\t\t:\n\t\t\t<>No logs yet for this workout. Start by logging exercises.>\n\t}\n}\n\nWorkoutLogsSection.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(WorkoutLogsSection);","import React, {PureComponent} from \"react\";\nimport withStyles from '@mui/styles/withStyles';\nimport moment from \"moment\";\nimport {UrlBuilder} from \"../../core/url/UrlBuilder\";\nimport Button from \"@mui/material/Button\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Dialog from \"@mui/material/Dialog\";\nimport Slide from \"@mui/material/Slide\";\nimport Box from \"@mui/material/Box\";\nimport Divider from \"@mui/material/Divider\";\nimport TextField from \"@mui/material/TextField\";\nimport Container from \"@mui/material/Container\";\nimport Grid from \"@mui/material/Grid\";\nimport WorkoutSmallNote from \"./partials/WorkoutSmallNote\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Fade from \"@mui/material/Fade\";\nimport Skeleton from '@mui/material/Skeleton';\nimport WorkoutCompleteEditForm from \"./partials/WorkoutCompleteEditForm\";\nimport {withRouter} from \"../../hooks/withRouter\";\nimport {createStyles} from \"@mui/styles\";\nimport {ButtonGroup} from \"@mui/material\";\nimport MoreVertIcon from \"@mui/icons-material/MoreVert\";\nimport WorkoutLogsSection from \"./partials/WorkoutLogsSection\";\n\nconst useStyles = theme => (\ncreateStyles({\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n\tappBar: {\n\t\tposition: 'relative',\n\t},\n\ttitle: {\n\t\tmarginLeft: theme.spacing(2),\n\t\tflex: 100,\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tnested: {\n\t\tpaddingLeft: theme.spacing(5),\n\t},\n\tmedia: {\n\t\theight: 125,\n\t},\n\tcontainer: {\n\t\tbackgroundColor: theme.palette.background.default,\n\t},\n}));\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\n/**\n * {@see App} This class is strongly tied to it. The app class decides this bloop's visibility.\n */\nclass WorkoutCompleteBloop extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'setUser': Updates the user -> needed to finish the workout.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The workout to show. None initially. We fetch it later.\n\t\t\t */\n\t\t\tworkout: null,\n\t\t\t/**\n\t\t\t * All the exercises in the workout.\n\t\t\t */\n\t\t\tallExercises: [],\n\t\t\t/**\n\t\t\t * Whether the modal is open. False initially. We never want the bloop to open automatically.\n\t\t\t */\n\t\t\tisOpen: false,\n\t\t\t/**\n\t\t\t * Whether this bloop is considered to be loading (i.e. the workout is being fetched). Initially true.\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Is the workout currently being submitted? (i.e. has the submit button been pressed?)\n\t\t\t */\n\t\t\tisSubmissionLoading: false,\n\t\t\t/**\n\t\t\t * The workout comment.\n\t\t\t */\n\t\t\tworkoutComment: null,\n\t\t\t/**\n\t\t\t * The popup for a log comment.\n\t\t\t */\n\t\t\tdropDownCommentAnchorEl: null,\n\t\t\t/**\n\t\t\t * Is it open?\n\t\t\t */\n\t\t\tisConfirmationDialogOpen: false,\n\t\t\t/**\n\t\t\t * The data stored by the child component. Used so that we don't lose the state when it's remounted.\n\t\t\t */\n\t\t\tworkoutsData: {},\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (this.state.isOpen !== nextProps.isOpen) {\n\t\t\tif (nextProps.isOpen) {\n\t\t\t\tthis.openSelf();\n\t\t\t} else {\n\t\t\t\tthis.setState({\n\t\t\t\t\tisOpen: false,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\t\n\topenSelf = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true,\n\t\t\tisOpen: true,\n\t\t\tisConfirmationDialogOpen: false, // reset\n\t\t});\n\n\t\tconst response = await fetch(\n\t\t\tUrlBuilder.workout.currentWorkoutApi()\n\t\t).then(res => res.json());\n\n\t\tlet workout = response.data;\n\t\t\n\t\tthis.setState({\n\t\t\tworkout: workout,\n\t\t\tworkoutComment: workout.comment,\n\t\t\tallExercises: this.getAllExercisesFromRoutine(workout),\n\t\t\tisLoading: false,\n\t\t});\n\t}\n\n\tgetAllExercisesFromRoutine(workout) {\n\t\tlet exercises = [];\n\t\tworkout?.routines.forEach(routine => {\n\t\t\troutine.exercises.forEach(exercise => {\n\t\t\t\texercises.push(exercise);\n\t\t\t})\n\t\t});\n\t\treturn exercises;\n\t}\n\t\n\thandleFinishWorkoutButton = async (e) => {\n\t\tif (this.state.workout?.routines?.length > 0) { // If there is anything to submit, then open the dialog.\n\t\t\tthis.openConfirmationDialog(true);\n\t\t} else {\n\t\t\tawait this.finishWorkout(e);\n\t\t}\n\t}\n\n\tfinishWorkout = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet workout = {\n\t\t\tdateTimeEnded: moment().format('L, h:mm:ss A'),\n\t\t\tcomment: this.state.workoutComment,\n\t\t};\n\t\t\n\t\t// This will just cancel the workout.\n\t\tawait fetch(UrlBuilder.workout.finishWorkoutApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(workout)\n\t\t})\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.setState({\n\t\t\t\t\tisSubmissionLoading: false,\n\t\t\t\t});\n\n\t\t\t\tlet user = response.data;\n\t\t\t\t\n\t\t\t\tthis.props.setUser(user);\n\n\t\t\t\tif (this.state.workout.routines.length > 0) { // Only redirect if you had some exercises to show.. (backend logic duplication)\n\t\t\t\t\tthis.props.navigate(UrlBuilder.workout.workoutDetailPage(this.state.workout.id, true));\n\t\t\t\t}\n\t\t\t})\n\t}\n\n\tsaveWorkoutNotes = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tlet workout = {\n\t\t\tcomment: this.state.workoutComment ?? event.target.value,\n\t\t};\n\n\t\tawait fetch(UrlBuilder.workout.currentWorkoutApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(workout)\n\t\t})\n\t}\n\n\tsetWorkoutCommentState = (event) => {\n\t\tthis.setState({\n\t\t\tworkoutComment: event.target.value ?? '',\n\t\t});\n\t}\n\t\n\topenConfirmationDialog = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisConfirmationDialogOpen: isOpen,\n\t\t});\n\t}\n\t\n\tsaveCachedWorkoutsData = (data) => {\n\t\tthis.setState({\n\t\t\tworkoutsData: data,\n\t\t})\n\t}\n\n\trenderBody() {\n\t\tconst numberOfExercisesDone = this.state.allExercises.length;\n\n\t\tlet workoutDuration, timeRange, title;\n\n\t\tif (this.state.workout?.isPastWorkout) {\n\t\t\ttitle = 'Started at'\n\t\t\tlet dateTimeMoment = moment(this.state.workout.dateTimeStarted);\n\t\t\tworkoutDuration = dateTimeMoment.format('h:mm a');\n\t\t\ttimeRange = dateTimeMoment.format('DD/MM/YYYY')\n\t\t} else {\n\t\t\tconst dateTimeStarted = moment(this.state.workout.dateTimeStarted);\n\t\t\tconst dateTimeEnded = moment(this.state.workout.dateTimeEnded);\n\t\t\tlet workoutDurationObj = moment.duration(dateTimeEnded.diff(dateTimeStarted));\n\t\t\t\n\t\t\ttitle = 'Duration';\n\t\t\tworkoutDuration = moment.utc(workoutDurationObj.asMilliseconds()).format('HH:mm');\n\t\t\ttimeRange = dateTimeStarted.format('LT') + \" - \" + dateTimeEnded.format('LT');\n\t\t}\n\n\t\treturn <>\n\t\t\t this.openConfirmationDialog(false)}\n\t\t\t/>\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\tTime started: {moment(this.state.workout.dateTimeStarted).fromNow() + ' - ' + moment(this.state.workout.dateTimeStarted).calendar()}\n\t\t\t \n\n\t\t\t\n\n\t\t\t \n\n\t\t\t\n\t\t\t \n\t\t\t\n\n\t\t\t \n\t\t>;\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.workout === null;\n\t}\n\n\trenderLoadingSkeletons = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n \n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.workout?.isPastWorkout ? 'Logging Past Workout' : 'Active Workout'}\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{this.hasNeverBeenFetched() ?\n\t\t\t\t\t\t\tthis.renderLoadingSkeletons()\n\t\t\t\t\t\t\t: this.renderBody()\n\t\t\t\t\t\t}\n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t
\n\t\t\t\t\tClose \n\n\t\t\t\t\t{this.state.workout?.isPastWorkout ?\n\t\t\t\t\t\t 0 ? : null}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{this.state.workout?.routines?.length > 0 ? 'Complete Workout' : 'Quit Workout'}\n\t\t\t\t\t\t \n\t\t\t\t\t\t:\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t{this.state.isSubmissionLoading && !this.state.isConfirmationDialogOpen ?\n\t\t\t\t\t\t\t\t\t<> Loading... >\n\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t<>{this.state.workout?.routines?.length > 0 ? 'Complete Workout' : 'Quit Workout'}>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t{this.state.workout?.routines?.length > 0 &&\n\t\t\t\t\t\t\t this.openConfirmationDialog(true)}\n\t\t\t\t\t\t\t\tdisabled={this.state.isLoading || this.state.isSubmissionLoading}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t \n\t\t\t\n );\n\t}\n}\n\t\nexport default withStyles(useStyles)(withRouter(WorkoutCompleteBloop));","export class Regexer {\n\tstatic getEmailRegex() {\n\t\treturn /^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n\t}\n\t\n\tstatic testEmail(email) {\n\t\treturn Regexer.getEmailRegex().test(email);\n\t}\n\t\n\tstatic getPasswordRegex() {\n\t\treturn /^(?=.*\\d).{6,}$/;\n\t}\n\n\tstatic testPassword(password) {\n\t\treturn Regexer.getPasswordRegex().test(password);\n\t}\n}","import React, {PureComponent} from 'react';\nimport {UserContext} from \"../../core/UserContext\";\nimport Avatar from '@mui/material/Avatar';\nimport Button from '@mui/material/Button';\nimport CssBaseline from '@mui/material/CssBaseline';\nimport TextField from '@mui/material/TextField';\nimport Box from '@mui/material/Box';\nimport LockOutlinedIcon from '@mui/icons-material/LockOutlined';\nimport Typography from '@mui/material/Typography';\nimport withStyles from '@mui/styles/withStyles';\nimport Container from '@mui/material/Container';\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport {Regexer} from \"../../core/auth/Regexer\";\nimport {PageDataContext} from \"../../core/PageDataContext\";\nimport {Divider, IconButton, InputAdornment} from \"@mui/material\";\nimport {Visibility, VisibilityOff} from \"@mui/icons-material\";\nimport {withRouter} from \"../../hooks/withRouter\";\nimport {Utils} from \"../../core/util/Utils\";\nimport {Link as RouterLink} from \"react-router-dom\";\nimport {UrlBuilder} from \"../../core/url/UrlBuilder\";\nimport Link from \"@mui/material/Link\";\nimport Grid from \"@mui/material/Grid\";\nimport GoogleIcon from \"@mui/icons-material/Google\";\nimport AppleIcon from \"@mui/icons-material/Apple\";\nimport PersonIcon from \"@mui/icons-material/Person\";\nimport moment from \"moment\";\n\nconst useStyles = theme => ({\n\tpaper: {\n\t\tmarginTop: theme.spacing(4),\n\t\tdisplay: 'flex',\n\t\tflexDirection: 'column',\n\t\talignItems: 'center',\n\t},\n\tavatar: {\n\t\tmargin: theme.spacing(1),\n\t\tbackgroundColor: '#2d3b4a',\n\t},\n\tform: {\n\t\twidth: '100%', // Fix IE 11 issue.\n\t\tmarginTop: theme.spacing(1),\n\t},\n\tsubmit: {\n\t\tmargin: theme.spacing(3, 0, 2),\n\t},\n});\n\nfunction Copyright() {\n\treturn (\n\t\t
\n\t\t\t{'Copyright © '}\n\t\t\t{'My Work in Progress '}\n\t\t\t{new Date().getFullYear()}\n\t\t\t{'.'}\n\t\t\t \n\t\t\t \n\t\t\t\tPrivacy Policy\n\t\t\t\n\t\t \n\t);\n}\n\nclass Signin extends PureComponent {\n\n\t/**\n\t * @param props No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * Used as a component 'isLoading' indicator.\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * Indicates whether someone tried to sign in with a user that was not found.\n\t\t\t */\n\t\t\tisUserNotFound: false,\n\t\t\t/**\n\t\t\t * Indicates whether someone tried to sign in with a user that exists but whose password is wrong.\n\t\t\t */\n\t\t\tisPasswordIncorrect: false,\n\t\t\t/**\n\t\t\t * Do we show the password field?\n\t\t\t */\n\t\t\tshowPassword: false,\n\t\t\tisSigningUpAsGuest: false,\n\t\t};\n\n\t\tthis.updateInputValue = this.updateInputValue.bind(this);\n\t\tthis.submit = this.submit.bind(this);\n\t}\n\t\n\tcomponentDidMount() {\n\t\tthis.context.setPageData({\n\t\t\ttitle: \"Sign In to My Work in Progress\"\n\t\t});\n\t}\n\n\tupdateInputValue(event) {\n\t\tthis.setState({\n\t\t\t[event.target.name]: event.target.value,\n\t\t\tisUserNotFound: false,\n\t\t\tisPasswordIncorrect: false,\n\t\t});\n\t}\n\n\tredirectToGoogleAuth = () => {\n\t\twindow.location.href = '/api/users/google-signin';\n\t}\n\n\tredirectToAppleAuth = () => {\n\t\twindow.location = '/api/users/apple-signin';\n\t}\n\t\n\tasync submit(event, setUser) {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tlet signinUser = {\n\t\t\temail: this.state.email,\n\t\t\tpassword: this.state.password,\n\t\t\tkeepSignedIn: this.state.rememberMe\n\t\t};\n\n\t\tlet response = await fetch('/api/users/signin', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(signinUser)\n\t\t});\n\n\t\tlet jsonResponse = await response.json();\n\t\t\n\t\tif (response.status === 200) {\n\t\t\tsetUser(jsonResponse['data']);\n\t\t\tthis.props.navigate('/');\n\t\t} else if (jsonResponse.message === \"user_not_found\") {\n\t\t\tthis.setState({\n\t\t\t\tisUserNotFound: true,\n\t\t\t\tisPasswordIncorrect: false,\n\t\t\t\tisLoading: false,\n\t\t\t})\n\t\t} else if (jsonResponse.message === \"incorrect_password\") {\n\t\t\tthis.setState({\n\t\t\t\tisPasswordIncorrect: true,\n\t\t\t\tisUserNotFound: false,\n\t\t\t\tisLoading: false,\n\t\t\t})\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tisLoading: false,\n\t\t\t})\n\t\t}\n\t}\n\n\tshowPassword = () => {\n\t\tthis.setState({\n\t\t\tshowPassword: !this.state.showPassword,\n\t\t});\n\t}\n\n\tsignUpAsGuest = async (event, setUser) => {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n\t\tthis.setState({\n\t\t\tisSigningUpAsGuest: true\n\t\t});\n\n\t\tlet guestUser = {\n\t\t\tdateCreated: moment().format('L, h:mm:ss A'),\n\t\t};\n\n\t\tlet response = await fetch(UrlBuilder.user.SignUpAsGuestApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(guestUser)\n\t\t});\n\n\t\tlet jsonResponse = await response.json();\n\n\t\tif (response.status === 200) {\n\t\t\tsetUser(jsonResponse['data']);\n\t\t\tthis.props.navigate('/');\n\t\t}\n\n\t\tthis.setState({\n\t\t\tisSigningUpAsGuest: false,\n\t\t})\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tlet isEmailInvalid = false;\n\t\tlet emailFieldHelperText = null;\n\t\tif (this.state.email && !Regexer.testEmail(this.state.email)) {\n\t\t\tisEmailInvalid = true;\n\t\t\temailFieldHelperText = 'Please enter a valid email'\n\t\t} else if (this.state.isUserNotFound) {\n\t\t\tisEmailInvalid = true;\n\t\t\temailFieldHelperText = 'Email not found'\n\t\t}\n\n\t\tlet isPasswordInvalid = false;\n\t\tlet passwordFieldHelperText = null;\n\t\tif (this.state.isPasswordIncorrect) {\n\t\t\tisPasswordInvalid = true;\n\t\t\tpasswordFieldHelperText = 'Password is incorrect'\n\t\t}\n\n\t\tlet isSubmitDisabled = !this.state.email || !Regexer.testEmail(this.state.email) || !this.state.password;\n\t\t\n\t\treturn (\n
\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t
\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t
\n\t\t\t\t\t\tSign in\n\t\t\t\t\t \n\t\t\t\t\t
\n\t\t\t\t
\n\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n );\n\t}\n}\n\nSignin.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(Signin));","import React, {PureComponent} from 'react';\nimport { UserContext } from \"../../core/UserContext\";\nimport Avatar from '@mui/material/Avatar';\nimport Button from '@mui/material/Button';\nimport CssBaseline from '@mui/material/CssBaseline';\nimport TextField from '@mui/material/TextField';\nimport Link from '@mui/material/Link';\nimport Grid from '@mui/material/Grid';\nimport Box from '@mui/material/Box';\nimport LockOutlinedIcon from '@mui/icons-material/LockOutlined';\nimport Typography from '@mui/material/Typography';\nimport withStyles from '@mui/styles/withStyles';\nimport Container from '@mui/material/Container';\nimport {Regexer} from \"../../core/auth/Regexer\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport {PageDataContext} from \"../../core/PageDataContext\";\nimport {Divider, IconButton, InputAdornment} from \"@mui/material\";\nimport {Visibility, VisibilityOff} from \"@mui/icons-material\";\nimport {withRouter} from \"../../hooks/withRouter\";\nimport {Link as RouterLink} from \"react-router-dom\";\nimport {UrlBuilder} from \"../../core/url/UrlBuilder\";\nimport GoogleIcon from \"@mui/icons-material/Google\";\nimport AppleIcon from \"@mui/icons-material/Apple\";\nimport {Utils} from \"../../core/util/Utils\";\nimport moment from \"moment\";\nimport PersonIcon from \"@mui/icons-material/Person\";\n\nconst useStyles = theme => ({\n paper: {\n marginTop: theme.spacing(4),\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n },\n avatar: {\n margin: theme.spacing(1),\n backgroundColor: '#2d3b4a',\n },\n form: {\n width: '100%', // Fix IE 11 issue.\n marginTop: theme.spacing(3),\n },\n submit: {\n margin: theme.spacing(3, 0, 2),\n },\n});\n\nfunction Copyright() {\n return (\n
\n {'Copyright © '}\n \n My Work in Progress\n {' '}\n {new Date().getFullYear()}\n {'.'}\n \n \n Privacy Policy\n \n \n );\n}\n\n\nclass Signup extends PureComponent {\n static displayName = Signup.name;\n\n /**\n * @param props No params needed.\n */\n constructor(props) {\n super(props);\n this.state ={\n /**\n * Used as a component 'isLoading' indicator.\n */\n isLoading: false,\n /**\n * Indicates whether the email is already taken by someone else.\n */\n isEmailAlreadyTaken: false,\n /**\n * Do we show the password field?\n */\n showPassword: false,\n isSigningUpAsGuest: false,\n };\n\n this.updateInputValue = this.updateInputValue.bind(this);\n this.submit = this.submit.bind(this);\n }\n\n componentDidMount() {\n this.context.setPageData({\n title: \"Sign Up to My Work in Progress\"\n });\n }\n\n updateInputValue(event) {\n this.setState({\n [event.target.name]: event.target.value,\n isEmailAlreadyTaken: false,\n });\n }\n\n redirectToGoogleAuth = () => {\n window.location = '/api/users/google-signin';\n }\n\n redirectToAppleAuth = () => {\n window.location = '/api/users/apple-signin';\n }\n\n async submit(event, setUser) {\n event.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n this.setState({\n isLoading: true\n });\n\n let signupUser = {\n name: this.state.name,\n email: this.state.email,\n password: this.state.password,\n dateCreated: moment().format('L, h:mm:ss A'),\n };\n\n let response = await fetch('/api/users/signup', {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(signupUser)\n });\n\n let jsonResponse = await response.json();\n\n if (response.status === 200) {\n setUser(jsonResponse['data']);\n this.props.navigate('/');\n } else if (jsonResponse.message === \"email_already_taken\") {\n this.setState({\n isEmailAlreadyTaken: true,\n isLoading: false,\n })\n } else {\n this.setState({\n isLoading: false,\n })\n }\n }\n\n showPassword = () => {\n this.setState({\n showPassword: !this.state.showPassword,\n });\n }\n\n signUpAsGuest = async (event, setUser) => {\n event.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n this.setState({\n isSigningUpAsGuest: true\n });\n\n let guestUser = {\n dateCreated: moment().format('L, h:mm:ss A'),\n };\n\n let response = await fetch(UrlBuilder.user.SignUpAsGuestApi(), {\n method: 'POST',\n headers: {\n 'Accept': 'application/json',\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(guestUser)\n });\n\n let jsonResponse = await response.json();\n\n if (response.status === 200) {\n setUser(jsonResponse['data']);\n this.props.navigate('/');\n }\n\n this.setState({\n isSigningUpAsGuest: false,\n })\n }\n\n render() {\n const { classes } = this.props;\n\n let isEmailInvalid = false;\n let emailFieldHelperText = null;\n if (this.state.email && !Regexer.testEmail(this.state.email)) {\n isEmailInvalid = true;\n emailFieldHelperText = 'Please enter a valid email'\n } else if (this.state.isEmailAlreadyTaken) {\n isEmailInvalid = true;\n emailFieldHelperText = 'Email is already taken, please try another one';\n }\n\n let isPasswordInvalid = false;\n let passwordFieldHelperText = null;\n if (this.state.password && !Regexer.testPassword(this.state.password)) {\n isPasswordInvalid = true;\n passwordFieldHelperText = 'Password must have a minimum length of 6 and contain a number.';\n }\n \n let isSubmitDisabled = !this.state.name || !this.state.email || isEmailInvalid || !this.state.password || isPasswordInvalid;\n\n return (\n
\n \n \n
\n \n \n
\n Sign Up\n \n
\n
\n \n \n \n \n );\n }\n}\n\nSignup.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(Signup));","import React, {PureComponent} from \"react\";\nimport withStyles from '@mui/styles/withStyles';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {PageDataContext} from \"../../../core/PageDataContext\";\nimport Grid from \"@mui/material/Grid\";\nimport WorkoutSmallNote from \"../partials/WorkoutSmallNote\";\nimport Box from \"@mui/material/Box\";\nimport moment from \"moment\";\nimport {Typography} from \"@mui/material\";\nimport Zoom from \"@mui/material/Zoom\";\nimport Divider from \"@mui/material/Divider\";\nimport DeleteIcon from \"@mui/icons-material/Delete\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport Button from \"@mui/material/Button\";\nimport Skeleton from '@mui/material/Skeleton';\nimport {SentimentVeryDissatisfied, SentimentVerySatisfied} from \"@mui/icons-material\";\nimport SentimentSatisfiedIcon from \"@mui/icons-material/SentimentSatisfied\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport WorkoutCompleteEditForm from \"../partials/WorkoutCompleteEditForm\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport Slide from \"@mui/material/Slide\";\nimport WorkoutLogsSection from \"../partials/WorkoutLogsSection\";\n\nconst useStyles = theme => ({\n\tnested: {\n\t\tpaddingLeft: theme.spacing(5),\n\t},\n\ttabBar: {\n\t\twidth: '100%',\n\t\tleft: 0,\n\t\tposition: 'fixed',\n\t\t'z-index' :99,\n\t},\n\tmedia: {\n\t\theight: 125,\n\t},\n\tbutton: {\n\t\tminWidth: '30px',\n\t\twidth: '30px',\n\t\theight: '30px',\n\t\tpadding: '0',\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\nclass WorkoutDetail extends PureComponent {\n\t/**\n\t * No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The workout to be fetched if a workout is active.\n\t\t\t */\n\t\t\tworkout: null,\n\t\t\t/**\n\t\t\t * Indicates whether this component is in a loading state.\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Whether the graphs are being fetched.\n\t\t\t */\n\t\t\tisGraphsLoading: true,\n\t\t\t/**\n\t\t\t * Should we show stuff saying \"Workout done!\"?\n\t\t\t */\n\t\t\tshowWorkoutDoneMessage: Boolean((new URLSearchParams(this.props.location.search)).get('just_finished') !== null),\n\t\t\t/**\n\t\t\t * The congrats message\n\t\t\t */\n\t\t\tworkoutDoneMessage: this.getWorkoutDoneMessage(),\n\t\t\t/**\n\t\t\t * Is the delete confirmation dialog open?\n\t\t\t */\n\t\t\tisDeleteConfirmationDialogOpen: false,\n\t\t\t/**\n\t\t\t * The popup for a log comment.\n\t\t\t */\n\t\t\tdropDownCommentAnchorEl: null,\n\t\t\t/**\n\t\t\t * Is the edit form open?\n\t\t\t */\n\t\t\tisWorkoutEditFormOpen: false,\n\t\t};\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.fetchWorkout();\n\t}\n\n\tgetNumberOfExercisesDone = (workout) => {\n\t\tlet total = 0;\n\t\tworkout.routines?.forEach(routine => {\n\t\t\ttotal += routine.exercises.length;\n\t\t});\n\t\treturn total;\n\t}\n\n\tfetchWorkout = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tconst response = await fetch(UrlBuilder.workout.workoutsApi(this.props.params.workoutId))\n\t\t\t.then(response => response.json());\n\n\t\tlet workout = response.data;\n\n\t\tthis.setState({\n\t\t\tworkout: workout,\n\t\t\tisLoading: false\n\t\t});\n\t\tthis.context.setPageData({\n\t\t\ttitle: workout.name,\n\t\t});\n\t}\n\n\tgetWorkoutDoneMessage = () => {\n\t\tlet messages = [\n\t\t\t'Good work!',\n\t\t\t'Good job!',\n\t\t\t'Great work!',\n\t\t\t'Great job!',\n\t\t];\n\n\t\treturn messages[Math.floor(Math.random() * messages.length)];\n\t}\n\t\n\tdeleteSelf = async (e) => {\n\t\te.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n\t\tawait fetch(UrlBuilder.workout.workoutsApi(this.state.workout.id), {\n\t\t\tmethod: 'DELETE',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t});\n\n\t\tthis.props.navigate('/');\n\t}\n\n\topenWorkoutDeleteConfirmationDialog = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisDeleteConfirmationDialogOpen: isOpen\n\t\t})\n\t}\n\t\n\trenderMood = (moodLevel) => {\n\t\tswitch (moodLevel) {\n\t\t\tcase 1:\n\t\t\t\treturn
;\n\t\t\tcase 2:\n\t\t\t\treturn
;\n\t\t\tcase 3:\n\t\t\t\treturn
\n\t\t}\n\t}\n\n\topenWorkoutEditForm = (isOpen, shouldRefresh = false) => {\n\t\tif (shouldRefresh) {\n\t\t\tthis.fetchWorkout();\n\t\t}\n\t\tthis.setState({\n\t\t\tisWorkoutEditFormOpen: isOpen,\n\t\t});\n\t}\n\n\trenderDeleteConfirmationDialog = () => {\n\t\tif (!this.state.isDeleteConfirmationDialogOpen) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn
this.openWorkoutDeleteConfirmationDialog(false)} TransitionComponent={Transition}>\n\t\t\t{\"Are you sure you want to delete this workout?\"} \n\t\t\t\n\t\t\t\t\n\t\t\t\t\tDeleting it will remove its corresponding logs. This cannot be undone.\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t this.openWorkoutDeleteConfirmationDialog(false)}>\n\t\t\t\t\tCancel\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\tDelete\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\t\n\trenderPage = () => {\n\t\tconst {classes} = this.props;\n\n\t\tconst numberOfExercisesDone = this.getNumberOfExercisesDone(this.state.workout);\n\n\t\tconst dateTimeStarted = moment(this.state.workout.dateTimeStarted);\n\t\tconst dateTimeEnded = moment(this.state.workout.dateTimeEnded);\n\t\tlet workoutDurationObj = moment.duration(dateTimeEnded.diff(dateTimeStarted));\n\t\tlet workoutDuration = moment.utc(workoutDurationObj.asMilliseconds()).format('HH:mm');\n\t\tlet timeRange = dateTimeStarted.format('LT') + \" - \" + dateTimeEnded.format('LT');\n\t\t\n\t\treturn <>\n\t\t\t
\n\n\t\t\t{this.state.showWorkoutDoneMessage ?\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.workoutDoneMessage} 🏆\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t : null\n\t\t\t}\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t \n\t\t\t\n\t\t\t{this.renderDeleteConfirmationDialog()}\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tDate: {dateTimeStarted.fromNow() + ' - ' + dateTimeStarted.calendar()}\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t this.openWorkoutDeleteConfirmationDialog(true)}\n size=\"small\"\n\t\t\t\t\t\tclassName={classes.button}\n\t\t\t\t\t>\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t this.openWorkoutEditForm(true)}\n size=\"small\"\n\t\t\t\t\t\tclassName={classes.button}\n\t\t\t\t\t>\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t{this.state.workout.moodLevel ?\n\t\t\t\t\n\t\t\t\t\tMood: \n\t\t\t\t\t\n\t\t\t\t\t\t{this.renderMood(this.state.workout.moodLevel)}\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t: null\n\t\t\t}\n\n\t\t\t{this.state.workout.comment ?\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\tNotes: {this.state.workout.comment}\n\t\t\t\t\t \n\t\t\t\t> : null\n\t\t\t}\n\n\t\t\t this.openWorkoutEditForm(false, shouldRefresh)}\n\t\t\t/>\n\n\t\t\t\n\t\t\t \n\t\t\t\n\n\t\t\t \n\t\t>;\n\t}\n\n\trenderLoadingSkeletons = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t \n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\t\n\trender() {\n\t\treturn \n\t\t\t{this.state.isLoading ?\n\t\t\t\tthis.renderLoadingSkeletons()\n\t\t\t\t: this.renderPage()\n\t\t\t}\n\t\t
\n\t}\n}\n\nWorkoutDetail.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(WorkoutDetail));","import React, {PureComponent} from 'react'\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Backdrop from \"@mui/material/Backdrop\";\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport Dialog from \"@mui/material/Dialog\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport FormControl from \"@mui/material/FormControl\";\nimport InputLabel from \"@mui/material/InputLabel\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport withStyles from '@mui/styles/withStyles';\nimport Button from \"@mui/material/Button\";\nimport Input from \"@mui/material/Input\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport IconButton from \"@mui/material/IconButton\";\nimport ClearIcon from '@mui/icons-material/Clear';\nimport Chip from \"@mui/material/Chip\";\nimport Avatar from \"@mui/material/Avatar\";\nimport Typography from \"@mui/material/Typography\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Slide from \"@mui/material/Slide\";\nimport Box from \"@mui/material/Box\";\nimport ColorSelector from \"../../core/utils/ColorSelector\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport {blue} from \"@mui/material/colors\";\n\nconst useStyles = theme => ({\n\tmodal: {\n\t\tdisplay: 'flex',\n\t\talignItems: 'center',\n\t\tjustifyContent: 'center',\n\t},\n\tformControl: {\n\t\tmargin: theme.spacing(1),\n\t\tminWidth: 200,\n\t},\n\tbutton: {\n\t\tmarginTop: theme.spacing(1),\n\t\tmarginBottom: theme.spacing(1),\n\t\tmarginRight: theme.spacing(1)\n\t},\n\tsuggestions: {\n\t\tdisplay: 'flex',\n\t\tjustifyContent: 'center',\n\t\tflexWrap: 'wrap',\n\t\t'& > *': {\n\t\t\tmargin: theme.spacing(0.5),\n\t\t},\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\nclass MeasurementNewOrEditFormPopUp extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'measurement': The measurement.\n\t * 'measurements': All the measurements (used for filtering).\n\t * 'isOpen': Says whether this modal is open.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * 'isOpen': Says whether this modal is open.\n\t\t\t */\n\t\t\tisOpen: this.props.isOpen,\n\n\t\t\t/**\n\t\t\t * The measurement to edit, if any. Otherwise it's a 'new' form.\n\t\t\t */\n\t\t\tmeasurement: this.props.measurement,\n\t\t\t\n\t\t\t/**\n\t\t\t * The name of the measurement.\n\t\t\t */\n\t\t\tname: this.props.measurement ? this.props.measurement.name : '',\n\t\t\t\n\t\t\t/**\n\t\t\t * Is it currently being submitted?\n\t\t\t */\n\t\t\tisSubmissionLoading: false,\n\t\t\t\n\t\t\t/**\n\t\t\t * All the measurements the user has. This is used to filter them out from suggestions.\n\t\t\t */\n\t\t\tmeasurements: [],\n\t\t\t/**\n\t\t\t * The measurement color.\n\t\t\t */\n\t\t\tcolor: props.measurement?.color ?? (props.measurement ? ColorPicker.pick(props.measurement.name) : blue[500]),\n\t\t};\n\t}\n\n\tsubmitWithEnter = (e) => {\n\t\tif (e.key === 'Enter') {\n\t\t\tthis.submit(e);\n\t\t}\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (this.state.isOpen !== Boolean(nextProps.isOpen)) {\n\t\t\tthis.setState({\n\t\t\t\tisOpen: Boolean(nextProps.isOpen),\n\t\t\t\tmeasurement: nextProps.measurement,\n\t\t\t\tname: nextProps.measurement ? nextProps.measurement.name : '',\n\t\t\t\tmeasurements: nextProps.measurements ?? [],\n\t\t\t\tcolor: nextProps.measurement?.color ?? (nextProps.measurement ? ColorPicker.pick(nextProps.measurement.name) : blue[500]),\n\t\t\t});\n\t\t}\n\t}\n\n\tsubmit = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\t\t\n\t\tlet measurementToSave = {\n\t\t\tName: this.state.name,\n\t\t\tColor: this.state.color,\n\t\t};\n\n\t\tif (this.state.measurement) {\n\t\t\tmeasurementToSave['Id'] = this.state.measurement.id;\n\t\t}\n\t\t\n\t\tawait fetch(UrlBuilder.measurements.measurementsApi(), {\n\t\t\tmethod: this.state.measurement ? 'PUT' : 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(measurementToSave)\n\t\t}).then(this.props.closeSelfFunc);\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: false,\n\t\t});\n\t}\n\n\tupdateStateInputValues = (event) => {\n\t\tlet state = {\n\t\t\t[event.target.name]: event.target.value,\n\t\t};\n\n\t\tif (event.target.name === 'name') {\n\t\t\tstate['color'] = ColorPicker.pick(event.target.value);\n\t\t}\n\n\t\tthis.setState(state);\n\t}\n\n\tclearStateInputValue = (fieldName) => {\n\t\tthis.setState({\n\t\t\t[fieldName]: '',\n\t\t});\n\t}\n\t\n\tupdateNameState = (name) => {\n\t\tthis.setState({\n\t\t\tname: name,\n\t\t\tcolor: ColorPicker.pick(name),\n\t\t});\n\t}\n\n\tupdateStateInputColorValue = (color) => {\n\t\tthis.setState({\n\t\t\tcolor: color,\n\t\t});\n\t}\n\t\n\tgetSuggestions = () => {\n\t\tlet suggestions = [\n\t\t\t'Weight',\n\t\t\t'BMI',\n\t\t\t'Fat Percentage',\n\t\t\t'Muscle Percentage',\n\t\t\t'Arm Size',\n\t\t\t'Leg Size',\n\t\t\t'Forearm Size',\n\t\t\t'Chest Size',\n\t\t];\n\t\t\n\t\treturn suggestions.filter(suggestion => {\n\t\t\tfor (let i = 0; i < this.state.measurements.length; i++) {\n\t\t\t\tif (this.state.measurements[i].name === suggestion) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t})\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\t\t\n\t\treturn (\n this.props.closeSelfFunc(false)}\n\t\t\t\tcloseAfterTransition\n\t\t\t\tBackdropComponent={Backdrop}\n\t\t\t\tBackdropProps={{\n\t\t\t\t\ttimeout: 250,\n\t\t\t\t}}\n\t\t\t\tTransitionComponent={Transition}\n >\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.measurement ? 'Edit Body Measurement' : 'Track New Body Measurement'}\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tName \n\t\t\t\t\t\t this.clearStateInputValue('name')} size=\"large\">\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t/>\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tSuggestions:\n\t\t\t\t\t \n\n\t\t\t\t\t\n\t\t\t\t\t\t{this.getSuggestions().map(suggestion =>\n\t\t\t\t\t\t\t{suggestion.charAt(0)}}\n\t\t\t\t\t\t\t\tlabel={suggestion}\n\t\t\t\t\t\t\t\tcolor=\"secondary\"\n\t\t\t\t\t\t\t\tonClick={() => this.updateNameState(suggestion)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t
\n\n\t\t\t\t\t\n\n\t\t\t\t\t\n\t\t\t\t\t\t Color\n\t\t\t\t\t \n\n\t\t\t\t\t\n\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t this.props.closeSelfFunc(false)}>\n\t\t\t\t\t\tClose\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t\t{this.state.isSubmissionLoading ?\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t Loading...\n\t\t\t\t\t\t\t
:\n\t\t\t\t\t\t\t'Save'\n\t\t\t\t\t\t}\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n );\n\t}\n}\n\nexport default withStyles(useStyles)(MeasurementNewOrEditFormPopUp);","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport Fade from \"@mui/material/Fade\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport ListItemIcon from \"@mui/material/ListItemIcon\";\nimport DeleteIcon from '@mui/icons-material/Delete';\nimport ListItemText from \"@mui/material/ListItemText\";\nimport Menu from \"@mui/material/Menu\";\nimport MoreVertIcon from \"@mui/icons-material/MoreVert\";\nimport IconButton from \"@mui/material/IconButton\";\nimport CardHeader from \"@mui/material/CardHeader\";\nimport Divider from \"@mui/material/Divider\";\nimport CardActions from \"@mui/material/CardActions\";\nimport Card from \"@mui/material/Card\";\nimport Typography from \"@mui/material/Typography\";\nimport {Avatar} from \"@mui/material\";\nimport {ColorPicker} from \"../../../core/colors/ColorPicker\";\nimport CardActionArea from \"@mui/material/CardActionArea\";\nimport Button from \"@mui/material/Button\";\nimport CardContent from \"@mui/material/CardContent\";\nimport LineGraph from \"../../graphs/LineGraph\";\nimport Collapse from \"@mui/material/Collapse\";\nimport {ExpandLess, ExpandMore} from \"@mui/icons-material\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport EditIcon from \"@mui/icons-material/Edit\";\n\nconst useStyles = theme => ({\n\tfloatRight: {\n\t\tfloat: 'right'\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n});\n\nclass MeasurementCard extends PureComponent {\n\t/**\n\t * @param props Containing:\n\t * 'measurement': The measurement.\n\t * 'deleteSelfFunc': Deletes itself.\n\t * 'editSelfFunc': Opens edit form for itself.\n\t * 'dragHandle': The handle icon for dragging this card to reorder it.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The measurement.\n\t\t\t */\n\t\t\tmeasurement: this.props.measurement,\n\t\t\t/**\n\t\t\t * The dropdown menu for each measurement card.\n\t\t\t */\n\t\t\tdropDownMenuAnchorEl: null,\n\t\t\t/**\n\t\t\t * Is the collapse open?\n\t\t\t */\n\t\t\tisCollapseOpen: true,\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\tmeasurement: nextProps.measurement,\n\t\t\tdropDownMenuAnchorEl: null\n\t\t});\n\t}\n\n\topenDropdownMenu = (event) => {\n\t\tevent.stopPropagation();\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: event.currentTarget,\n\t\t});\n\t};\n\n\tcloseDropdownMenu = (event) => {\n\t\tevent.stopPropagation();\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: null\n\t\t});\n\t};\n\n\ttriggerCollapse = () => {\n\t\tthis.setState({\n\t\t\tisCollapseOpen: !this.state.isCollapseOpen\n\t\t});\n\t}\n\t\n\tgetLineGraphData = () => {\n\t\tlet x = [];\n\t\tlet y = [];\n\n\t\tthis.state.measurement.logs.forEach(log => {\n\t\t\tx.push((new Date(log.dateCreated)).toLocaleDateString());\n\t\t\ty.push(log.value);\n\t\t});\n\n\t\treturn {\n\t\t\tx: x.reverse(),\n\t\t\ty: y.reverse(),\n\t\t\tlabel: this.state.measurement ? this.state.measurement.name : ''\n\t\t};\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\t\t{this.state.measurement.name}\n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t\tavatar={\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.measurement.name.charAt(0).toUpperCase()}\n\t\t\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t\t// subheader={'hai'}\n\t\t\t\t\taction={\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t\tonClick={() => this.props.setBloopOpen(this.state.measurement)}\n\t\t\t\t/>\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t\t{}\n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t this.props.setBloopOpen(this.state.measurement)}>Open \n\t\t\t\t\t\n\t\t\t\t\t\n\n\t\t\t\t\t
\n\t\t\t\t\t\t{this.state.isCollapseOpen ? : }\n\t\t\t\t\t \n\n\t\t\t\t\t{this.props.dragHandle}\n\t\t\t\t\n\t\t\t\n\t\t);\n\t}\n}\n\nexport default withStyles(useStyles)(withRouter(MeasurementCard));","export class GraphQLQueryBuilder{\n\t\n\tstatic fullMeasurements(measurementId = null) {\n\t\tconst uniqueString = measurementId ? '(id: ' + measurementId + ')' : '';\n\t\t\n\t\treturn JSON.stringify({ query:\n`\nquery {\n\tmeasurements ${uniqueString} {\n\t\tid\n\t\tname,\n\t\tcolor,\n\t\tsortNumber,\n\t\tlogs {\n\t\t\tid,\n\t\t\tvalue,\n\t\t\tdateCreated\n\t\t}\n\t}\n}\n`\n\t\t});\n\t}\n}","import React, {PureComponent} from \"react\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Paper from \"@mui/material/Paper\";\nimport Table from \"@mui/material/Table\";\nimport TableHead from \"@mui/material/TableHead\";\nimport TableRow from \"@mui/material/TableRow\";\nimport TableCell from \"@mui/material/TableCell\";\nimport TableBody from \"@mui/material/TableBody\";\nimport IconButton from \"@mui/material/IconButton\";\nimport MoreVertIcon from \"@mui/icons-material/MoreVert\";\nimport Menu from \"@mui/material/Menu\";\nimport Fade from \"@mui/material/Fade\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport ListItemIcon from \"@mui/material/ListItemIcon\";\nimport EditIcon from \"@mui/icons-material/Edit\";\nimport ListItemText from \"@mui/material/ListItemText\";\nimport TableContainer from \"@mui/material/TableContainer\";\nimport DeleteIcon from '@mui/icons-material/Delete';\nimport withStyles from '@mui/styles/withStyles';\nimport Typography from \"@mui/material/Typography\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport {DatePicker, LocalizationProvider} from \"@mui/lab\";\nimport SaveIcon from '@mui/icons-material/Save';\nimport TextField from \"@mui/material/TextField\";\nimport moment from \"moment\";\nimport AdapterDateFns from \"@mui/lab/AdapterDateFns\";\n\nconst useStyles = theme => ({\n\troot: {\n\t\twidth: '100%',\n\t\tpadding: 0\n\t},\n});\n\nclass MeasurementLogsTable extends PureComponent {\n\n\t/**\n\t * @param props Containing:\n\t * 'exercise': The exercise you want to add an exercise-log for.\n\t * 'isLoading': Is component loading?\n\t * 'refreshParentFunc': The function to call to refresh the parent.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The measurement whose logs we want to show.\n\t\t\t */\n\t\t\tmeasurement: this.props.measurement,\n\t\t\t/**\n\t\t\t * Indicates which 'edit log' popup is being shown.\n\t\t\t */\n\t\t\tlogInEditableMode: null,\n\t\t\t/**\n\t\t\t * The dropdown menu for each exercise.\n\t\t\t */\n\t\t\tdropDownMenuAnchorEl: null,\n\t\t\t/**\n\t\t\t * Indicates whether it's loading.\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * Is a submission being loaded?\n\t\t\t */\n\t\t\tisSubmissionLoading: false,\n\n\t\t\t/**\n\t\t\t * The value of the 'new' log row.\n\t\t\t */\n\t\t\tvalue: this.props.measurement && this.props.measurement.logs[0] ? this.props.measurement.logs[0].value : 0,\n\t\t\t/**\n\t\t\t * The dateCreated value of the 'new' log row.\n\t\t\t */\n\t\t\tdateCreated: new Date(),\n\t\t\t\n\t\t\t/** \n\t\t\t * Only not null if logInEditableMode is not null. This is the edited value of a log if it's in edit mode.\n\t\t\t */\n\t\t\teditedLogValue: null,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\tmeasurement: nextProps.measurement,\n\t\t\tisLoading: nextProps.isLoading\n\t\t});\n\t}\n\t\n\tsubmitNewLog = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet measurementLogToSave = {\n\t\t\tMeasurementId: this.state.measurement.id,\n\t\t\tValue: this.state.value,\n\t\t\tDateCreated: moment(this.state.dateCreated).format('L, h:mm:ss A'),\n\t\t};\n\n\t\tawait fetch(UrlBuilder.measurements.measurementLogsApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(measurementLogToSave)\n\t\t}).then(() => this.props.refreshParentFunc('New log created.'));\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: false,\n\t\t\tdateCreated: new Date(), // Reset timer (this is so that the same time is not used if another log is submitted).\n\t\t});\n\t}\n\n\tsubmitLogEdit = async (event) => {\n\t\tevent.preventDefault();\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: true,\n\t\t});\n\n\t\tlet measurementLogToUpdate = {\n\t\t\tId: this.state.logInEditableMode.id,\n\t\t\tValue: this.state.editedLogValue,\n\t\t};\n\n\t\tawait fetch(UrlBuilder.measurements.measurementLogsApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(measurementLogToUpdate)\n\t\t}).then(() => this.props.refreshParentFunc('Log updated.'));\n\n\t\tthis.setState({\n\t\t\tisSubmissionLoading: false,\n\t\t\teditedLogValue: null,\n\t\t\tlogInEditableMode: null,\n\t\t});\n\t}\n\n\tdeleteMeasurementLog = async (event, log) => {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\t\tevent.stopPropagation();\n\n\t\tawait fetch(UrlBuilder.measurements.measurementLogsApi(log.id), {\n\t\t\tmethod: 'DELETE',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t})\n\t\t\t.then(() => this.props.refreshParentFunc(\"Log successfully deleted.\"));\n\t}\n\n\topenDropdownMenu = (event) => {\n\t\tevent.preventDefault();\n\t\tevent.stopPropagation();\n\t\t\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: event.currentTarget\n\t\t});\n\t};\n\n\tcloseDropdownMenu = (e) => {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\t\n\t\tthis.setState({\n\t\t\tdropDownMenuAnchorEl: null\n\t\t});\n\t};\n\n\ttoggleEditableLog(e, log) {\n\t\te.preventDefault();\n\t\te.stopPropagation();\n\t\t\n\t\tthis.setState({\n\t\t\tlogInEditableMode: log,\n\t\t\teditedLogValue: log.value,\n\t\t\tdropDownMenuAnchorEl: null\n\t\t});\n\t}\n\n\tisToday = (someDateString) => {\n\t\tconst someDate = new Date(someDateString);\n\t\tconst today = new Date();\n\t\treturn (someDate.toDateString() === today.toDateString());\n\t}\n\n\tsetSelectedLog = (log) => {\n\t\tthis.setState({\n\t\t\tvalue: log.value,\n\t\t\tdateCreated: log.dateCreated,\n\t\t});\n\t}\n\n\thandleDateChange = (date) => {\n\t\tthis.setState({\n\t\t\tdateCreated: date\n\t\t});\n\t};\n\n\tupdateStateInputValues = (event) => {\n\t\tthis.setState({\n\t\t\t[event.target.name]: event.target.value\n\t\t});\n\t}\n\n\tsubmitWithEnter = (e) => {\n\t\tif (e.key === 'Enter') {\n\t\t\tthis.submitNewLog(e);\n\t\t}\n\t}\n\n\tsubmitEditWithEnter = (e) => {\n\t\tif (e.key === 'Enter') {\n\t\t\tthis.submitLogEdit(e);\n\t\t}\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\t\t\n\t\treturn (\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\tLog History: {this.state.isLoading ? : null}\n\t\t\t\t \n\n\t\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t{/*The top row (the head)*/}\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tDate \n\t\t\t\t\t\t\t\tValue \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/*Create new entry row*/}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t }\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{/*The logs in the measurement*/}\n\t\t\t\t\t\t\t{this.state.measurement?.logs.map((log, idx) =>\n\t\t\t\t\t\t\t\t this.setSelectedLog(log)}>\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{this.isToday(log.dateCreated) ?\n\t\t\t\t\t\t\t\t\t\t\t\t{(new Date(log.dateCreated)).toDateString()} \n\t\t\t\t\t\t\t\t\t\t\t\t: (new Date(log.dateCreated)).toDateString()\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{this.state.logInEditableMode && log.id === this.state.logInEditableMode.id ?\n\t\t\t\t\t\t\t\t\t\t\t e.stopPropagation()}\n\t\t\t\t\t\t\t\t\t\t\t\tonChange={this.updateStateInputValues}\n\t\t\t\t\t\t\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\t\t\t\t\t\t\tonKeyPress={this.submitEditWithEnter}\n\t\t\t\t\t\t\t\t\t\t\t\tInputLabelProps={{ shrink: true }}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{maxWidth: 80, marginTop: 0, marginBottom: 0}}\n\t\t\t\t\t\t\t\t\t\t\t\tsize='small'\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t{log.value}\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{this.state.logInEditableMode && log.id === this.state.logInEditableMode.id ?\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t this.toggleEditableLog(e, log)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\t this.deleteMeasurementLog(e, log)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t \n\t\t\t\t\t
\n\t\t\t\t \n\t\t\t
\n\t\t);\n\t}\n}\n\nexport default withStyles(useStyles)(MeasurementLogsTable);","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport Container from \"@mui/material/Container\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Dialog from \"@mui/material/Dialog\";\nimport AppBar from \"@mui/material/AppBar\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport CloseIcon from '@mui/icons-material/Close';\nimport Slide from \"@mui/material/Slide\";\nimport Snackbar from \"@mui/material/Snackbar\";\nimport Typography from \"@mui/material/Typography\";\nimport DialogContent from \"@mui/material/DialogContent\";\nimport RadioButtonCheckedIcon from \"@mui/icons-material/RadioButtonChecked\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport Box from \"@mui/material/Box\";\nimport {GraphQLQueryBuilder} from \"../../../core/url/GraphQLQueryBuilder\";\nimport MeasurementLogsTable from \"./MeasurementLogsTable\";\nimport LineGraph from \"../../graphs/LineGraph\";\nimport {UserContext} from \"../../../core/UserContext\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport { red } from '@mui/material/colors';\n\nconst useStyles = theme => ({\n\ttitle: {\n\t\tmarginLeft: theme.spacing(1),\n\t\tflex: 100,\n\t},\n\tcontainer: {\n\t\twidth: '100%',\n\t\tpadding: 0,\n\t\tbackgroundColor: theme.palette.background.default,\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\ttabMenu: {\n\t\tcolor: \"rgba(241,241,241,0.89)\",\n\t},\n\t'@keyframes flicker': {\n\t\tfrom: {\n\t\t\topacity: 1,\n\t\t},\n\t\tto: {\n\t\t\topacity: 0.4,\n\t\t},\n\t},\n\tflicker: {\n\t\tanimationName: '$flicker',\n\t\tanimationDuration: '1000ms',\n\t\tanimationIterationCount: 'infinite',\n\t\tanimationDirection: 'alternate',\n\t\tanimationTimingFunction: 'ease-in-out',\n\t\tcolor: red[300],\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn
;\n});\n\n/**\n * A card that can be popped up into fullscreen mode.\n */\nclass MeasurementDetailBloop extends PureComponent {\n\n\t/**\n\t * @param props Containing:\n\t * 'measurement': The measurement.\n\t * 'isOpen': Is this bloop open?\n\t * 'closeSelfFunc': Close self.\n\t * 'updateParentMeasurement': The function the parent's measurement.\n\t * 'switchMeasurementBloop': The function to switch which measurement is displayed here.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The measurement.\n\t\t\t */\n\t\t\tmeasurement: this.props.measurement,\n\t\t\t/**\n\t\t\t * Indicates whether this component is in a loading state.\n\t\t\t */\n\t\t\tisLoading: false,\n\t\t\t/**\n\t\t\t * Indicates whether the bloop is open.\n\t\t\t */\n\t\t\tisOpen: this.props.isOpen,\n\t\t\t/**\n\t\t\t * The message to show for the toast notification. Null if it shouldn't show. Initializing\n\t\t\t * this will cause the toast notification to show up.\n\t\t\t */\n\t\t\ttoastNotificationMessage: null,\n\t\t};\n\t}\n\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\tmeasurement: nextProps.measurement,\n\t\t\tisOpen: nextProps.isOpen,\n\t\t});\n\t}\n\n\tcomponentDidUpdate(prevProps, prevState, snapshot) {\n\t\t// Detects the browser back button and closes the detail popup.\n\t\twindow.onpopstate = e => {\n\t\t\tthis.props.closeSelfFunc();\n\t\t}\n\t}\n\n\t/**\n\t * @param event\n\t * @param reason\n\t */\n\tcloseToastNotification = (event, reason) => {\n\t\tif (reason === 'clickaway') {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\ttoastNotificationMessage: null,\n\t\t});\n\t};\n\n\trefresh = async (withMessage = null) => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\n\t\tawait fetch(UrlBuilder.graphql, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t}, // Add the ID of this measurement to fetch it only.\n\t\t\tbody: GraphQLQueryBuilder.fullMeasurements(this.state.measurement.id),\n\t\t})\n\t\t\t.then(res => res.json())\n\t\t\t.then(res => {\n\t\t\t\tlet measurement = res['data']['measurements'][0] ?? this.state.measurement;\n\t\t\t\tthis.props.updateParentMeasurement(measurement); // This will pass the new measurement to the bloop, so not need to set it here.\n\t\t\t\tthis.setState({\n\t\t\t\t\tisLoading: false,\n\t\t\t\t\ttoastNotificationMessage: withMessage,\n\t\t\t\t});\n\t\t\t});\n\t}\n\t\n\tgetLineGraphData = () => {\n\t\tlet x = [];\n\t\tlet y = [];\n\t\t\n\t\tthis.state.measurement.logs.forEach(log => {\n\t\t\tx.push((new Date(log.dateCreated)).toLocaleDateString());\n\t\t\ty.push(log.value);\n\t\t});\n\t\t\n\t\treturn {\n\t\t\tx: x.reverse(),\n\t\t\ty: y.reverse(),\n\t\t\tlabel: this.state.measurement ? this.state.measurement.name : ''\n\t\t};\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\t\t\n\t\treturn (\n
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.measurement?.name}\n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t{this.context.user && this.context.user.activeWorkout ?\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t this.context.openActiveWorkoutBloop(true)} className={classes.flicker}/>\n\t\t\t\t\t\t\t : null\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\n\n\t\t\t\t
\n\t\t\t\t\t\n\t\t\t\t\t\t\n\n\t\t\t\t\t\t{this.state.measurement ? : null}\n\n\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\n );\n\t}\n}\n\nMeasurementDetailBloop.contextType = UserContext;\n\nexport default withStyles(useStyles)(withRouter(MeasurementDetailBloop));","import React, {PureComponent} from 'react';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport withStyles from '@mui/styles/withStyles';\nimport {PageDataContext} from \"../../../core/PageDataContext\";\nimport Button from \"@mui/material/Button\";\nimport MeasurementNewOrEditFormPopUp from \"../partials/MeasurementNewOrEditFormPopUp\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Fade from \"@mui/material/Fade\";\nimport Box from \"@mui/material/Box\";\nimport Skeleton from '@mui/material/Skeleton';\nimport Grid from \"@mui/material/Grid\";\nimport IconButton from \"@mui/material/IconButton\";\nimport AddToPhotosIcon from \"@mui/icons-material/AddToPhotos\";\nimport Typography from \"@mui/material/Typography\";\nimport Zoom from \"@mui/material/Zoom\";\nimport Fab from \"@mui/material/Fab\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport MeasurementCard from \"../partials/MeasurementCard\";\nimport Snackbar from \"@mui/material/Snackbar\";\nimport MeasurementDetailBloop from \"../partials/MeasurementDetailBloop\";\nimport {UserLocalStorage} from \"../../../core/storage/UserLocalStorage\";\nimport {GraphQLQueryBuilder} from \"../../../core/url/GraphQLQueryBuilder\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport {DragDropContext, Draggable, Droppable} from \"react-beautiful-dnd\";\nimport UnfoldMoreIcon from \"@mui/icons-material/UnfoldMore\";\n\nconst useStyles = theme => ({\n\tspeedDialWrapper: {\n\t\tmargin: 0,\n\t\ttop: 'auto',\n\t\tright: 35,\n\t\tbottom: 80,\n\t\tleft: 'auto',\n\t\tposition: 'fixed',\n\t},\n\tlargeIcon: {\n\t\twidth: 200,\n\t\theight: 200,\n\t},\n\tsnackbar: {\n\t\tbottom: 75,\n\t\t[theme.breakpoints.down('sm')]: {\n\t\t\tbottom: 150,\n\t\t},\n\t},\n\tmedia: {\n\t\theight: 110,\n\t},\n});\n\nclass MeOverview extends PureComponent {\n\t/**\n\t * @param props No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * All the routines that we have available. These are fetched from the backend.\n\t\t\t */\n\t\t\tmeasurements: UserLocalStorage.get(this.getCacheKey()) ?? null,\n\t\t\t/**\n\t\t\t * Indicates loading or not\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Should we show the add measurement pop-up?\n\t\t\t */\n\t\t\tshowAddMeasurementPopUp: false,\n\t\t\t/**\n\t\t\t * The measurement that is being edited.\n\t\t\t */\n\t\t\tmeasurementInEditableMode: null,\n\t\t\t/**\n\t\t\t * The toast notification if it should show up. Null if it shouldn't show. Initializing\n\t\t\t * this will cause the toast notification to show up.\n\t\t\t */\n\t\t\ttoastNotificationObject: null,\n\t\t\t/**\n\t\t\t * The measurement ID we should open the bloop for. We use ID and not measurement to make sure the measurement we were given is not outdated.\n\t\t\t * This class will always hold the most up-to-date measurement.\n\t\t\t */\n\t\t\tmeasurementIdBloopOpen: null,\n\t\t};\n\t}\n\t\n\tgetCacheKey = () => {\n\t\treturn UrlBuilder.graphql + '/measurements';\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.context.setPageData({\n\t\t\ttitle: \"Me\"\n\t\t});\n\n\t\tif (this.hasNeverBeenFetched()) {\n\t\t\tthis.fetch().then(() => {\n\t\t\t\tthis.openBloopFromURLIfPossible();\n\t\t\t});\n\t\t} else {\n\t\t\tthis.openBloopFromURLIfPossible();\n\t\t\tthis.fetch(); // Don't try to open bloop after this.\n\t\t}\n\t}\n\n\topenBloopFromURLIfPossible = () => {\n\t\tlet measurementIdToOpen = (new URLSearchParams(this.props.location.search)).get('measurement');\n\t\tif (measurementIdToOpen && this.state.measurements) {\n\t\t\tfor (let i = 0; i < this.state.measurements.length; i++) {\n\t\t\t\tlet measurement = this.state.measurements[i];\n\n\t\t\t\tif (measurement.id == measurementIdToOpen) {\n\t\t\t\t\tthis.setState({\n\t\t\t\t\t\tmeasurementIdBloopOpen: measurement.id,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\topenAddMeasurementPopUp = () => {\n\t\tthis.setState({\n\t\t\tshowAddMeasurementPopUp: true\n\t\t});\n\t}\n\n\ttoggleEditableMeasurement(event, measurement) {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\t\tevent.stopPropagation(); // Prevents from click from propagating.\n\n\t\tthis.setState({\n\t\t\tmeasurementInEditableMode: measurement,\n\t\t});\n\t}\n\n\trefresh = () => {\n\t\tthis.fetch();\n\t}\n\t\n\tfetch = async () => {\n\t\tthis.setState({\n\t\t\tisLoading: true\n\t\t});\n\t\t\n\t\tconst response = await fetch(UrlBuilder.graphql, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: GraphQLQueryBuilder.fullMeasurements(),\n\t\t}).then(res => res.json());\n\n\t\tthis.setState({\n\t\t\tmeasurements: response.data.measurements ?? [],\n\t\t\tisLoading: false\n\t\t});\n\t\t\n\t\tUserLocalStorage.set(this.getCacheKey(), response.data.measurements ?? []);\n\t}\n\n\tdeleteMeasurement = async (event, measurement) => {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\t\tevent.stopPropagation(); // Prevents from click from propagating.\n\n\t\tawait fetch(UrlBuilder.measurements.measurementsApi(measurement.id), {\n\t\t\tmethod: 'DELETE',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t})\n\t\t\t.then(this.refresh)\n\n\t\tthis.showMeasurementDeletedToastNotification(measurement);\n\t}\n\n\tundeleteMeasurement = async(event, measurement) => {\n\t\tevent.preventDefault(); // Prevents it from reloading the page and adding the params to the URL.\n\n\t\tlet measurementView = {\n\t\t\tid: measurement.id,\n\t\t\tisDeleted: false,\n\t\t};\n\n\t\tawait fetch(UrlBuilder.measurements.measurementsApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'accept': 'application/json',\n\t\t\t\t'content-type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(measurementView)\n\t\t})\n\t\t\t.then(this.refresh);\n\n\t\tthis.closeToastNotification();\n\t}\n\n\tshowMeasurementDeletedToastNotification = (measurement) => {\n\t\tconst {classes} = this.props\n\t\tthis.setState({\n\t\t\ttoastNotificationObject: this.undeleteMeasurement(e, measurement)}>\n\t\t\t\t\t\tUndo\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t/>\n\t\t});\n\t}\n\n\tcloseToastNotification = (e, reason) => {\n\t\tthis.setState({\n\t\t\ttoastNotificationObject: null\n\t\t});\n\t}\n\n\tgetMeasurementFromId = (measurementId) => {\n\t\tif (measurementId) {\n\t\t\tfor (let i = 0; i < this.state.measurements.length; i++) {\n\t\t\t\tlet measurement = this.state.measurements[i];\n\t\t\t\tif (measurement.id == measurementId) {\n\t\t\t\t\treturn measurement;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\thandleBloopOpen = (measurement) => {\n\t\tif (measurement) {\n\t\t\tmeasurement = this.getMeasurementFromId(measurement.id);\n\n\t\t\tthis.setState({\n\t\t\t\tmeasurementIdBloopOpen: measurement.id,\n\t\t\t});\n\n\t\t\tlet currentUrlParams = new URLSearchParams(window.location.search);\n\t\t\tcurrentUrlParams.set('measurement', measurement.id);\n\t\t\tthis.props.navigate(window.location.pathname + \"?\" + currentUrlParams.toString());\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tmeasurementIdBloopOpen: null,\n\t\t\t});\n\n\t\t\tthis.props.navigate(window.location.pathname);\n\t\t}\n\t};\n\n\tswitchMeasurementBloop = (measurement) => {\n\t\tthis.setState({\n\t\t\tmeasurementIdBloopOpen: null,\n\t\t});\n\n\t\t// Wait before doing this, to get the full animation :)\n\t\tsetTimeout(() => this.handleBloopOpen(measurement), 100);\n\t}\n\n\tupdateMeasurement = (measurement) => {\n\t\tlet measurements = this.state.measurements;\n\n\t\tfor (let i = 0; i < this.state.measurements.length; i++) {\n\t\t\tlet measurementTemp = this.state.measurements[i];\n\n\t\t\tif (measurementTemp.id == measurement.id) {\n\t\t\t\tmeasurements[i] = measurement;\n\t\t\t\tthis.setState({\n\t\t\t\t\tmeasurements: measurements,\n\t\t\t\t});\n\t\t\t\tthis.forceUpdate(); // We do this because this is a PureComponent and the routine's memory address didn't change, so we need to force it.\n\t\t\t\tUserLocalStorage.set(this.getCacheKey(), measurements);\n\t\t\t}\n\t\t}\n\t}\n\n\treorder = (list, startIndex, endIndex) => {\n\t\tconst result = Array.from(list);\n\t\tconst [removed] = result.splice(startIndex, 1);\n\t\tresult.splice(endIndex, 0, removed);\n\n\t\treturn result;\n\t};\n\n\tonDragEnd = async (result) => {\n\t\t// dropped outside the list\n\t\tif (!result.destination) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet measurements = this.reorder(\n\t\t\tthis.state.measurements,\n\t\t\tresult.source.index,\n\t\t\tresult.destination.index\n\t\t);\n\n\t\tthis.setState({\n\t\t\tmeasurements: measurements\n\t\t});\n\t\tthis.forceUpdate();\n\n\t\tawait this.submitMeasurementsReordering(measurements);\n\t};\n\n\tsubmitMeasurementsReordering = async (measurements) => {\n\t\tlet measurementViews = [];\n\t\tmeasurements.forEach((measurement, idx) => measurementViews.push({\n\t\t\tid: measurement.id,\n\t\t\tsortNumber: idx,\n\t\t}));\n\n\t\tawait fetch(UrlBuilder.measurements.measurementsBulkApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(measurementViews)\n\t\t});\n\n\t\tUserLocalStorage.remove(this.getCacheKey()); // Let's stale the localStorage so that the next time a refresh occurs it doesnt look choppy.\n\t}\n\n\trenderPage = () => {\n\t\tif (this.state.measurements && this.state.measurements.length > 0) {\n\t\t\treturn this.renderOverview();\n\t\t}\n\t\treturn this.renderNoRoutines();\n\t}\n\n\ttoggleCloseAllPopups = (shouldRefresh = true) => {\n\t\tthis.setState({\n\t\t\tshowAddMeasurementPopUp: false,\n\t\t\tmeasurementInEditableMode: null,\n\t\t});\n\t\tif (shouldRefresh) {\n\t\t\tthis.fetch();\n\t\t}\n\t}\n\n\thasNeverBeenFetched = () => {\n\t\treturn this.state.measurements === null;\n\t}\n\n\trenderLoadingSkeletons = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\t\n\trenderOverview = () => {\n\t\treturn <>\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t{(provided, snapshot) => (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.state.measurements.map((measurement, index) => {\n\t\t\t\t\t\t\t\tlet key = \"measurement_\" + measurement.id;\n\t\t\t\t\t\t\t\treturn \n\t\t\t\t\t\t\t\t\t{(provided, snapshot) => (\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t this.toggleEditableMeasurement(e, measurement)}\n\t\t\t\t\t\t\t\t\t\t\t\tdeleteSelfFunc={(e) => this.deleteMeasurement(e, measurement)}\n\t\t\t\t\t\t\t\t\t\t\t\tsetBloopOpen={this.handleBloopOpen}\n\t\t\t\t\t\t\t\t\t\t\t\tdragHandle={ }\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t \n\t\t\t\t\t)}\n\t\t\t\t \n\t\t\t \n\n\t\t\t this.handleBloopOpen(null)}\n\t\t\t\tupdateParentMeasurement={this.updateMeasurement}\n\t\t\t\tswitchMeasurementBloop={this.switchMeasurementBloop}\n\t\t\t/>\n\t\t>;\n\t}\n\n\trenderNoRoutines = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t\n\t\t\t\t\tYou have no measurements yet\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tCreate a measurement \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\t\t>;\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn <>\n\t\t\t\n\t\t\t\t \n\t\t\t \n\n\t\t\t \n\n\t\t\t \n\n\t\t\t{\n\t\t\t\tthis.hasNeverBeenFetched() ?\n\t\t\t\t\tthis.renderLoadingSkeletons()\n\t\t\t\t\t: this.renderPage()\n\t\t\t}\n\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\n\t\t\t{this.state.toastNotificationObject}\n\t\t>;\n\t}\n}\n\nMeOverview.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(MeOverview));","import { useEffect } from \"react\";\nimport { useLocation } from \"react-router-dom\";\n\n/**\n * https://reactrouter.com/web/guides/scroll-restoration\n */\nexport default function ScrollToTop() {\n\tconst { pathname } = useLocation();\n\n\tuseEffect(() => {\n\t\twindow.scrollTo(0, 0);\n\t}, [pathname]);\n\n\treturn null;\n}","export default __webpack_public_path__ + \"static/media/badge_2.5f703cd5.png\";","import React, {PureComponent} from 'react';\nimport {UserContext} from \"../../../core/UserContext\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\n\n/**\n * Just submits that the user has no notifications after X seconds. This is because the parent\n * class already has another context. This is a workaround to load the UserContext.\n */\nclass SubmitHasNoNotifications extends PureComponent {\n\t\n\tcomponentDidMount() {\n\t\tif (this.context.user.hasNotifications) {\n\t\t\tsetTimeout(() => this.submitUserHasNoNotifications(), 2000);\n\t\t}\n\t}\n\n\tsubmitUserHasNoNotifications = async () => {\n\t\tlet user = {\n\t\t\thasNotifications: false,\n\t\t};\n\n\t\tlet response = await fetch(UrlBuilder.user.currentUserApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(user)\n\t\t}).then(res => res.json());\n\n\t\tthis.context.setUser(response['data']);\n\t}\n\t\n\trender() {\n\t\treturn <>>;\n\t}\n}\n\nSubmitHasNoNotifications.contextType = UserContext;\n\nexport default SubmitHasNoNotifications;","export default __webpack_public_path__ + \"static/media/gpay-button.1b52fe70.svg\";","import React, {Component} from 'react';\nimport {UserContext} from \"../../../core/UserContext\";\nimport withStyles from '@mui/styles/withStyles';\nimport Typography from \"@mui/material/Typography\";\nimport Button from \"@mui/material/Button\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {\n\tDivider,\n\tListItem,\n\tListItemText,\n\tDialog,\n\tDialogTitle,\n\tDialogContent,\n\tDialogActions,\n\tTextField,\n\tListItemAvatar, Checkbox, FormControlLabel,\n} from \"@mui/material\";\nimport List from \"@mui/material/List\";\nimport Badge2 from \"../../../res/img/badge_2.png\";\nimport GPayButton from \"../../../res/img/gpay-button.svg\";\nimport Grid from \"@mui/material/Grid\";\nimport FavoriteIcon from '@mui/icons-material/Favorite';\nimport moment from \"moment\";\nimport * as Sentry from \"@sentry/react\";\nimport Skeleton from '@mui/material/Skeleton';\nimport { Alert } from '@mui/material';\nimport {UserLocalStorage} from \"../../../core/storage/UserLocalStorage\";\nimport Slide from \"@mui/material/Slide\";\n\nconst useStyles = theme => ({\n\tlargeIcon: {\n\t\twidth: 100,\n\t\theight: 100,\n\t},\n\tcheckboxGroup: {\n\t\tdisplay: 'flex',\n\t\tflexDirection: 'column',\n\t\talignItems: 'flex-start',\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\nclass DonationDialog extends Component {\n\n\t/**\n\t * @param props Containing:\n\t * 'isOpen': Says whether this dialog is open.\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * Is it open?\n\t\t\t */\n\t\t\tisOpen: false,\n\t\t\t/**\n\t\t\t * Used as a component 'isLoading' indicator.\n\t\t\t */\n\t\t\tisDonationsLoading: true,\n\t\t\t/**\n\t\t\t * The fetched donations list.\n\t\t\t */\n\t\t\tdonations: [],\n\t\t\t/**\n\t\t\t * The supporter name to submit.\n\t\t\t */\n\t\t\tsupporterDisplayName: '',\n\t\t\t/**\n\t\t\t * Used as a component 'isLoading' indicator.\n\t\t\t */\n\t\t\tisProductsLoading: false,\n\t\t\t/**\n\t\t\t * The products you can buy (the supporter product/s).\n\t\t\t */\n\t\t\tproducts: [],\n\t\t\t/**\n\t\t\t * Any error message to show for the payment.\n\t\t\t */\n\t\t\tpaymentErrorMessage: null,\n\t\t\t/**\n\t\t\t * Any warning message to show for the payment.\n\t\t\t */\n\t\t\tpaymentWarningMessage: null,\n\t\t\t/**\n\t\t\t * Any success message to show for the payment.\n\t\t\t */\n\t\t\tpaymentSuccessMessage: null,\n\t\t\t/**\n\t\t\t * The checked product ID in the checkbox form.\n\t\t\t */\n\t\t\tcheckedProductId: 'supporter_badge_1_new',\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tif (this.state.isOpen !== Boolean(nextProps.isOpen)) {\n\t\t\tthis.setState({\n\t\t\t\tisOpen: Boolean(nextProps.isOpen),\n\t\t\t\tsupporterDisplayName: this.context.user.name ?? '',\n\t\t\t\tpaymentErrorMessage: null,\n\t\t\t\tpaymentWarningMessage: null,\n\t\t\t\tpaymentSuccessMessage: null,\n\t\t\t});\n\t\t\t\n\t\t\tif (nextProps.isOpen) {\n\t\t\t\tthis.refresh(); // Only refresh when it was closed before and now its opening.\n\t\t\t}\n\t\t}\n\t}\n\n\trefresh = () => {\n\t\tthis.setState({\n\t\t\tpaymentErrorMessage: null,\n\t\t\tpaymentWarningMessage: null,\n\t\t\tpaymentSuccessMessage: null,\n\t\t});\n\t\tthis.fetchPurchaseProducts();\n\t\tthis.fetchDonationsList();\n\t\tthis.scanForUnacknowledgedPayments();\n\t}\n\t\n\tscanForUnacknowledgedPayments = async () => {\n\t\tif ('getDigitalGoodsService' in window) {\n\t\t\tlet service;\n\t\t\ttry {\n\t\t\t\tservice = await window.getDigitalGoodsService('https://play.google.com/billing');\n\t\t\t} catch (e) {\n\t\t\t\tSentry.captureException(e);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (service) {\n\t\t\t\t// Gets all the purchases made by current user. See interface at https://github.com/WICG/digital-goods/blob/main/explainer.md#api-v20\n\t\t\t\tconst existingPurchases = await service.listPurchases();\n\t\t\t\tfor (const p of existingPurchases) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Try to acknowledge any payments if you can.\n\t\t\t\t\t\tconst isSuccess = await this.attemptPurchaseAcknowledgment(p.itemId, p.purchaseToken);\n\t\t\t\t\t\tif (isSuccess) {\n\t\t\t\t\t\t\tthis.setState({\n\t\t\t\t\t\t\t\tpaymentSuccessMessage: \"Donation successfully made. Thank you!\",\n\t\t\t\t\t\t\t\tpaymentWarningMessage: null,\n\t\t\t\t\t\t\t\tpaymentErrorMessage: null,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\tresolveDisplayName = () => {\n\t\tlet displayName;\n\t\tif (UserLocalStorage.get('paymentDisplayName')) { // First check if one was saved.\n\t\t\tdisplayName = UserLocalStorage.get('paymentDisplayName');\n\t\t} else if (this.state.supporterDisplayName) {\n\t\t\tdisplayName = this.state.supporterDisplayName;\n\t\t} else {\n\t\t\tdisplayName = this.context.user.name;\n\t\t}\n\t\treturn displayName;\n\t}\n\t\n\tattemptPurchaseAcknowledgment = async (productName, purchaseToken) => {\n\t\tlet isSuccess = await fetch(UrlBuilder.playPayments.acknowledge(productName, purchaseToken, this.resolveDisplayName()), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t}).then(response => response.status === 200) // If 200 then its success.\n\t\t\t.catch(res => {return false;})\n\n\t\tif (isSuccess) {\n\t\t\tlet user = await fetch(UrlBuilder.user.currentUserApi())\n\t\t\t\t.then(response => response.status === 200 ? response.json() : null)\n\t\t\t\t.catch(res => {\n\t\t\t\t\treturn null;\n\t\t\t\t});\n\n\t\t\tthis.context.setUser(user);\n\t\t\tthis.refresh();\n\t\t\tUserLocalStorage.remove('paymentDisplayName'); // Success? then remove this.\n\t\t\treturn true;\n\t\t}\n\t\t\n\t\treturn false;\n\t}\n\n\tfetchDonationsList = async () => {\n\t\tthis.setState({\n\t\t\tisDonationsLoading: true\n\t\t});\n\n\t\tawait fetch(\n\t\t\tUrlBuilder.donations.donationsApi()\n\t\t)\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.setState({\n\t\t\t\t\tisDonationsLoading: false,\n\t\t\t\t\tdonations: response['data'] ?? [],\n\t\t\t\t});\n\t\t\t});\n\t}\n\n\tfetchPurchaseProducts = async () => {\n\t\tif ('getDigitalGoodsService' in window) {\n\t\t\tlet service;\n\t\t\ttry {\n\t\t\t\tservice = await window.getDigitalGoodsService('https://play.google.com/billing');\n\t\t\t} catch (e) {\n\t\t\t\tSentry.captureException(e);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (service) {\n\t\t\t\tthis.setState({\n\t\t\t\t\tisProductsLoading: true\n\t\t\t\t});\n\n\t\t\t\tlet products = [];\n\t\t\t\t\n\t\t\t\t// Gets product details: https://github.com/WICG/digital-goods/blob/main/explainer.md#api-v20\n\t\t\t\tconst fetchedProducts = await service.getDetails(['supporter_badge_1_new', 'supporter_badge_2', 'supporter_subscription', 'supporter_subscription_2']).catch(e => {\n\t\t\t\t\tSentry.captureException(e);\n\t\t\t\t\treturn [];\n\t\t\t\t});\n\n\t\t\t\tfetchedProducts.forEach(product => {\n\t\t\t\t\t// Format the price according to the user locale.\n\t\t\t\t\tconst localizedPrice = new Intl.NumberFormat(\n\t\t\t\t\t\tnavigator.language,\n\t\t\t\t\t\t{style: 'currency', currency: product.price.currency}\n\t\t\t\t\t).format(product.price.value);\n\n\t\t\t\t\t// Render the price to the UI.\n\t\t\t\t\tproducts.push({\n\t\t\t\t\t\tid: product.itemId,\n\t\t\t\t\t\ttitle: product.title,\n\t\t\t\t\t\tprice: localizedPrice,\n\t\t\t\t\t\tdescription: product.description,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\t\n\t\t\t\tthis.setState({\n\t\t\t\t\tisProductsLoading: false,\n\t\t\t\t\tproducts: products,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * https://developer.chrome.com/docs/android/trusted-web-activity/receive-payments-play-billing/\n\t */\n\tattemptPayment = async (productName = null) => {\n\t\tif (this.state.supporterDisplayName.trim() === '') {\n\t\t\tthis.setState({\n\t\t\t\tsupporterDisplayNameIsEmpty: true,\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tproductName = productName ?? 'supporter_badge_1_new';\n\t\t\n\t\tconst paymentMethodData = [{\n\t\t\tsupportedMethods: 'https://play.google.com/billing',\n\t\t\tdata: {\n\t\t\t\tsku: productName,\n\t\t\t}\n\t\t}];\n\n\t\tconst paymentDetails = {\n\t\t\ttotal: {\n\t\t\t\tlabel: `Total`,\n\t\t\t\tamount: {currency: `USD`, value: `0`}\n\t\t\t}\n\t\t};\n\t\t\n\t\tconst request = new PaymentRequest(paymentMethodData, paymentDetails);\n\t\t\n\t\tlet paymentMade = false;\n\n\t\ttry {\n\t\t\tSentry.captureMessage(\"1 - Payment showing initiated for \" + this.context.user.email);\n\t\t\t\n\t\t\tconst paymentResponse = await request.show();\n\t\t\t\n\t\t\tSentry.captureMessage(\"2 - Payment response obtained for \" + this.context.user.email);\n\n\t\t\t// We got this far if the payment was made, otherwise an error would've been thrown.\n\t\t\tconst {token: purchaseToken} = paymentResponse.details;\n\n\t\t\tSentry.captureMessage(\"3 - Payment made for \" + this.context.user.email + \" with token: \" + purchaseToken, {\n\t\t\t\textra: {\n\t\t\t\t\tresponse: JSON.stringify(paymentResponse),\n\t\t\t\t}\n\t\t\t});\n\t\t\t\n\t\t\tpaymentMade = true;\n\t\t\t\n\t\t\t// Now that the payment was made, we acknowledge the payment with the backend.\n\t\t\tconst isAcknowledgmentSuccess = await this.attemptPurchaseAcknowledgment(productName, purchaseToken);\n\n\t\t\tif (isAcknowledgmentSuccess) {\n\t\t\t\tSentry.captureMessage(\"4 - Payment acknowledged for \" + this.context.user.email + \" with token: \" + purchaseToken);\n\n\t\t\t\t// Optional: tell the PaymentRequest API the validation was\n\t\t\t\t// successful. The user-agent may show a \"payment successful\"\n\t\t\t\t// message to the user.\n\t\t\t\tconst paymentComplete = await paymentResponse.complete('success');\n\t\t\t\tSentry.captureMessage(\"5 - Payment response completed for \" + this.context.user.email);\n\n\t\t\t\tthis.setState({\n\t\t\t\t\tpaymentSuccessMessage: \"Donation successfully made. Thank you!\",\n\t\t\t\t\tpaymentWarningMessage: null,\n\t\t\t\t\tpaymentErrorMessage: null,\n\t\t\t\t});\n\t\t\t\t\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tSentry.captureMessage(\"4 - Payment acknowledged failed for \" + this.context.user.email + \" with token: \" + purchaseToken);\n\t\t\t\tconst paymentComplete = await paymentResponse.complete('fail');\n\t\t\t}\n\n\t\t} catch(e) {\n\t\t\tSentry.captureMessage(`${e.name}: ${e.message}`);\n\t\t\t// The purchase failed, and we can handle the failure here. AbortError\n\t\t\t// usually means a user cancellation\n\t\t\ttry {\n\t\t\t\tawait request.abort();\n\t\t\t} catch (e) {}\n\t\t}\n\n\t\tif (paymentMade) {\n\t\t\tthis.setState({\n\t\t\t\tpaymentSuccessMessage: null,\n\t\t\t\tpaymentWarningMessage: \"Your payment was made but is still being processed by Google Play. Please check back here in a few minutes and your Supporter's Badge should be ready. If you have any questions please contact us at myworkinprogress.app@gmail.com or leave a feedback message.\",\n\t\t\t\tpaymentErrorMessage: null,\n\t\t\t});\n\t\t\tUserLocalStorage.set('paymentDisplayName', this.state.supporterDisplayName);\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tpaymentSuccessMessage: null,\n\t\t\t\tpaymentWarningMessage: null,\n\t\t\t\tpaymentErrorMessage: \"Your payment failed. Please try again. If you have any questions please contact us at myworkinprogress.app@gmail.com or leave a feedback message.\",\n\t\t\t});\n\t\t}\n\t}\n\t\n\tupdateSupporterNameText = (event) => {\n\t\tthis.setState({\n\t\t\tsupporterDisplayName: event.target.value,\n\t\t\tsupporterDisplayNameIsEmpty: false,\n\t\t});\n\t}\n\n\thandleCheckboxChange = (event, value) => {\n\t\tthis.setState({ checkedProductId: value });\n\t};\n\n\trenderCheckboxes = () => {\n\t\tconst checkboxes = [];\n\n\t\tif (this.state.products[0]) {\n\t\t\tcheckboxes.push({id: 'supporter_badge_1_new', label: 'One-time payment - ' + this.state.products[0].price});\n\t\t}\n\n\t\tif (this.state.products[1]) {\n\t\t\tcheckboxes.push({id: 'supporter_badge_2', label: 'One-time payment - ' + this.state.products[1].price});\n\t\t}\n\t\t\n\t\tif (this.state.products[2]) {\n\t\t\tcheckboxes.push({ id: 'supporter_subscription', label: 'Monthly subscription - ' + this.state.products[2].price});\n\t\t}\n\t\t\n\t\tif (this.state.products[3]) {\n\t\t\tcheckboxes.push({ id: 'supporter_subscription_2', label: 'Monthly subscription - ' + this.state.products[3].price});\n\t\t}\n\n\t\treturn checkboxes.map((checkbox) => (\n\t\t\t this.handleCheckboxChange(event, checkbox.id)}\n\t\t\t\t\t/>\n\t\t\t\t}\n\t\t\t\tlabel={checkbox.label}\n\t\t\t/>\n\t\t));\n\t};\n\n\trender () {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n \n Support My Work in Progress \n \n \n \n Supporting the app encourages further development of the app and helps pay for server costs.\n \n\n \n\n\t {this.context.user.isSupporter &&\n\t\t <>\n\t\t\t \n\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t\t You are a supporter.\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t \n\t\t\t\t\t \n\t\t\t\t\t\t Thank you!\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\t\t\t \n\t\t >\n\t }\n\t \n {this.context.user.isSupporter ?\n\t You may donate again if you wish! \n\t :\n\t \n\t\t You can become a Supporter by purchasing the Supporter Badge. Besides being awesome, in return you join the Supporters List and get a shiny Supporter's badge!\n\t \n }\n\n \n \n {this.state.isProductsLoading ?\n \n :\n ('getDigitalGoodsService' in window && this.state.products.length > 0) ?\n \n\t \n\t\t \n\t \n\t \n\t\t \n\t\t\t \n\t\t\t\t \n\t\t\t\t\t {this.renderCheckboxes()}\n\t\t\t\t
\n\t\t\t \n\t\t\t \n\t\t\t \n\t\t\t\t this.attemptPayment(this.state.checkedProductId)}/>\n\t\t\t \n\t\t\t \n\t\t\t\t Secure payment done through Google Play Billing. Subscriptions can be cancelled anytime through Google Play. \n\t\t\t \n\t\t \n\t \n \n :\n \n \n Making a donation is unfortunately not possible in this version of the app.\n \n \n }\n\n {this.state.paymentErrorMessage ? <> {this.state.paymentErrorMessage} > : null}\n {this.state.paymentWarningMessage ? <> {this.state.paymentWarningMessage} > : null}\n {this.state.paymentSuccessMessage ? <> {this.state.paymentSuccessMessage} > : null}\n\n \n \n \n\n \n Supporters List:\n \n \n {this.state.isDonationsLoading ?\n <>\n \n \n \n >\n : this.state.donations.length === 0 ?\n \n No supporters yet. Become the first one!\n : null\n }\n \n \n {this.state.donations.map(supporter => \n \n \n \n \n \n \n )}\n
\n\n \n \n \n Close\n \n \n \n );\n\t}\n}\n\nDonationDialog.contextType = UserContext;\n\nexport default withStyles(useStyles)(DonationDialog);","import React, {PureComponent} from 'react';\nimport {UserContext} from \"../../core/UserContext\";\nimport withStyles from '@mui/styles/withStyles';\nimport Typography from \"@mui/material/Typography\";\nimport Box from \"@mui/material/Box\";\nimport TextField from \"@mui/material/TextField\";\nimport {Regexer} from \"../../core/auth/Regexer\";\nimport Button from \"@mui/material/Button\";\nimport {UrlBuilder} from \"../../core/url/UrlBuilder\";\nimport {PageDataContext} from \"../../core/PageDataContext\";\nimport {\n\tCardHeader,\n\tDivider,\n\tCardContent,\n\tCard,\n\tCardActions,\n\tListItem,\n\tRadio,\n\tRadioGroup,\n\tFormControlLabel,\n\tListItemAvatar,\n\tListItemText,\n\tDialog,\n\tDialogTitle,\n\tDialogContent,\n\tDialogActions,\n\tCheckbox,\n\tIconButton, Collapse, InputAdornment\n} from \"@mui/material\";\nimport SecurityIcon from '@mui/icons-material/Security';\nimport AccountCircleIcon from \"@mui/icons-material/AccountCircle\";\nimport {UnitPrettifier} from \"../../core/prettifier/UnitPrettifier\";\nimport List from \"@mui/material/List\";\nimport FormLabel from \"@mui/material/FormLabel\";\nimport FormControl from \"@mui/material/FormControl\";\nimport TuneIcon from '@mui/icons-material/Tune';\nimport FeedbackIcon from '@mui/icons-material/Feedback';\nimport InfoIcon from '@mui/icons-material/Info';\nimport DialogContentText from \"@mui/material/DialogContentText\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport Snackbar from \"@mui/material/Snackbar\";\nimport FavoriteIcon from '@mui/icons-material/Favorite';\nimport Badge2 from \"../../res/img/badge_2.png\";\nimport WhatshotIcon from '@mui/icons-material/Whatshot';\nimport Badge from \"@mui/material/Badge\";\nimport CallMadeIcon from '@mui/icons-material/CallMade';\nimport SubmitHasNoNotifications from \"./partials/SubmitHasNoNotifications\";\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport ExpandLessIcon from '@mui/icons-material/ExpandLess';\nimport DonationDialog from \"./partials/DonationDialog\";\nimport {withRouter} from \"../../hooks/withRouter\";\nimport Slide from \"@mui/material/Slide\";\nimport {Utils} from \"../../core/util/Utils\";\nimport {Alert, AlertTitle} from \"@mui/lab\";\nimport GoogleIcon from \"@mui/icons-material/Google\";\nimport {Visibility, VisibilityOff} from \"@mui/icons-material\";\nimport AppleIcon from \"@mui/icons-material/Apple\";\n\nconst useStyles = theme => ({\n\tpaper: {\n\t\tmarginTop: theme.spacing(8),\n\t\tdisplay: 'flex',\n\t\tflexDirection: 'column',\n\t\talignItems: 'center',\n\t},\n\tavatar: {\n\t\tmargin: theme.spacing(1),\n\t\tbackgroundColor: theme.palette.secondary.main,\n\t},\n\tform: {\n\t\twidth: '100%', // Fix IE 11 issue.\n\t\tmarginTop: theme.spacing(1),\n\t},\n\tsubmit: {\n\t\tmargin: theme.spacing(3, 0, 2),\n\t},\n\troot: {\n\t\t'& .MuiTextField-root': {\n\t\t\tmargin: theme.spacing(1),\n\t\t\twidth: '25ch',\n\t\t},\n\t},\n\tgrow: {\n\t\tflexGrow: 1,\n\t},\n\tnested: {\n\t\tpaddingLeft: theme.spacing(2),\n\t},\n});\n\nconst Transition = React.forwardRef(function Transition(props, ref) {\n\treturn ;\n});\n\nclass Settings extends PureComponent {\n\n\t/**\n\t * @param props No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * Is the submit feedback dialog open?\n\t\t\t */\n\t\t\tisSubmitFeedbackDialogOpen: false,\n\t\t\t/**\n\t\t\t * shouldIncludeFeedbackEmail?\n\t\t\t */\n\t\t\tshouldIncludeFeedbackEmail: true,\n\t\t\t/**\n\t\t\t * The message to show for the toast notification. Null if it shouldn't show. Initializing\n\t\t\t * this will cause the toast notification to show up.\n\t\t\t */\n\t\t\ttoastNotificationMessage: null,\n\t\t\t/**\n\t\t\t * Is the supporter dialog open?\n\t\t\t */\n\t\t\tisSupporterDialogOpen: false,\n\t\t\t/**\n\t\t\t * Is the announcements collapse open?\n\t\t\t */\n\t\t\tisAnnouncementsCollapseOpen: false,\n\t\t\t/**\n\t\t\t * Indicates whether the email is already taken by someone else.\n\t\t\t */\n\t\t\tisEmailAlreadyTaken: false,\n\t\t\t/**\n\t\t\t * Do we show the password field?\n\t\t\t */\n\t\t\tshowPassword: false,\n\t\t};\n\n\t\tthis.configSectionRef = React.createRef();\n\t\tthis.supporterSectionRef = React.createRef();\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.context.setPageData({\n\t\t\ttitle: \"My Settings\",\n\t\t});\n\n\t\tlet goToDonation = (new URLSearchParams(this.props.location.search)).has('goToDonation');\n\t\tif (goToDonation) {\n\t\t\tsetTimeout(() => this.scrollToSupporterSection(), 500);\n\t\t}\n\t}\n\n\t/**\n\t * @param event\n\t * @param reason\n\t */\n\tcloseToastNotification = (event, reason) => {\n\t\tif (reason === 'clickaway') {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\ttoastNotificationMessage: null,\n\t\t});\n\t};\n\t\n\tsetToastNotificationText = (text) => {\n\t\tthis.setState({\n\t\t\ttoastNotificationMessage: text,\n\t\t});\n\t}\n\n\tupdateStateInputValues = (event) => {\n\t\tif (event.target.type !== \"checkbox\") {\n\t\t\tthis.setState({\n\t\t\t\t[event.target.name]: event.target.value,\n\t\t\t\tisEmailAlreadyTaken: false,\n\t\t\t});\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\t[event.target.name]: event.target.checked,\n\t\t\t\tisEmailAlreadyTaken: false,\n\t\t\t});\n\t\t}\n\t}\n\n\tsubmitAccountDetailsWithEnter = (e, setUser, shouldSubmit) => {\n\t\tif (shouldSubmit && e.key === 'Enter') {\n\t\t\tthis.submitAccountDetailsUpdate(e, setUser);\n\t\t}\n\t}\n\n\tsubmitConvertGuestToUserWithEnter = (e, setUser, shouldSubmit) => {\n\t\tif (shouldSubmit && e.key === 'Enter') {\n\t\t\tthis.submitConvertGuestToUser(e, setUser);\n\t\t}\n\t}\n\n\tsubmitPasswordWithEnter = (e, setUser, shouldSubmit) => {\n\t\tif (shouldSubmit && e.key === 'Enter') {\n\t\t\tthis.submitPasswordUpdate(e, setUser);\n\t\t}\n\t}\n\n\tlogOut = async (event, setUser) => {\n\t\tevent.preventDefault(); // prevents it from reloading the page and adding the params to the url.\n\n\t\tawait fetch('/api/users/logout', {\n\t\t\tmethod: 'post',\n\t\t\theaders: {\n\t\t\t\t'accept': 'application/json',\n\t\t\t\t'content-type': 'application/json'\n\t\t\t},\n\t\t});\n\n\t\tsetUser(null);\n\t\tthis.props.navigate('/');\n\t}\n\t\n\tupdateUser = async (userModel, setUser) => {\n\t\tlet response = await fetch(UrlBuilder.user.currentUserApi(), {\n\t\t\tmethod: 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(userModel)\n\t\t});\n\n\t\tif (response.status === 200) {\n\t\t\tlet userData = await response.json();\n\t\t\tsetUser(userData['data']);\n\t\t}\n\t}\n\n\tsubmitPreferredUnitSystemId = async (event, user, setUser) => {\n\t\tevent.preventDefault(); // prevents it from reloading the page and adding the params to the url.\n\t\tevent.stopPropagation();\n\n\t\tlet value = event.target.value;\n\t\t\n\t\tlet userView = {\n\t\t\tpreferredUnitSystemId: value,\n\t\t};\n\n\t\tawait this.updateUser(userView, setUser);\n\n\t\tthis.setToastNotificationText('Unit system updated to ' + (value == UnitPrettifier.kg ? 'metric.' : 'imperial.'));\n\t}\n\n\tsubmitHasDarkModeEnabled = async (event, user, setUser) => {\n\t\tevent.preventDefault(); // prevents it from reloading the page and adding the params to the url.\n\t\tevent.stopPropagation();\n\n\t\tlet isEnabled = event.target.value === 'true';\n\t\t\n\t\tlet userView = {\n\t\t\thasDarkModeEnabled: isEnabled,\n\t\t};\n\n\t\tawait this.updateUser(userView, setUser);\n\n\t\tthis.setToastNotificationText(isEnabled ? 'Dark Mode enabled.' : 'Dark Mode disabled.');\n\t}\n\t\n\tsubmitConvertGuestToUser = async(e, setUser) => {\n\t\te.preventDefault();\n\n\t\tlet user = {\n\t\t\tname: this.state.name,\n\t\t\temail: this.state.email,\n\t\t\tpassword: this.state.password,\n\t\t};\n\n\t\tlet response = await fetch(UrlBuilder.user.convertGuestToUserApi(), {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(user)\n\t\t});\n\n\t\tlet jsonResponse = await response.json();\n\n\t\tif (response.status === 200) {\n\t\t\tsetUser(jsonResponse['data']);\n\t\t\tthis.setToastNotificationText('Account registered successfully.');\n\t\t\tthis.setState({\n\t\t\t\tpassword: null,\n\t\t\t\tname: null,\n\t\t\t\temail: null,\n\t\t\t})\n\t\t\treturn;\n\t\t}\n\n\t\tif (jsonResponse.message === \"email_already_taken\") {\n\t\t\tthis.setState({\n\t\t\t\tisEmailAlreadyTaken: true,\n\t\t\t})\n\t\t}\n\t}\n\n\tsubmitAccountDetailsUpdate = async (event, setUser) => {\n\t\tevent.preventDefault();\n\n\t\tlet userView = {\n\t\t\tname: this.state.name,\n\t\t};\n\t\t\n\t\tawait this.updateUser(userView, setUser);\n\n\t\tthis.setToastNotificationText('Details updated.')\n\t}\n\n\tsubmitPasswordUpdate = async (event, setUser) => {\n\t\tevent.preventDefault();\n\n\t\tlet userView = {\n\t\t\tpassword: this.state.password,\n\t\t};\n\n\t\tawait this.updateUser(userView, setUser);\n\n\t\tthis.setToastNotificationText('Password updated.')\n\t}\n\t\n\tsubmitFeedback = async (user) => {\n\t\tif (this.state.feedbackText) {\n\t\t\tlet feedbackView = {\n\t\t\t\ttext: this.state.feedbackText,\n\t\t\t};\n\n\t\t\tif (this.state.shouldIncludeFeedbackEmail) {\n\t\t\t\tfeedbackView['email'] = this.state.feedbackEmail ? this.state.feedbackEmail : user.email;\n\t\t\t}\n\n\t\t\tawait fetch(UrlBuilder.user.feedbackApi(), {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'Accept': 'application/json',\n\t\t\t\t\t'Content-Type': 'application/json'\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(feedbackView)\n\t\t\t});\n\t\t\t\n\t\t\tthis.setToastNotificationText('Thanks for your feedback!')\n\t\t}\n\t\t\n\t\tthis.openSubmitFeedbackDialog(false);\n\t}\n\t\n\topenSubmitFeedbackDialog = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisSubmitFeedbackDialogOpen: isOpen,\n\t\t});\n\t}\n\t\n\topenSupporterDialog = (isOpen) => {\n\t\tthis.setState({\n\t\t\tisSupporterDialogOpen: isOpen,\n\t\t});\n\t}\n\n\tscrollToSupporterSection = () => {\n\t\tif (this.supporterSectionRef && this.supporterSectionRef.current) {\n\t\t\tthis.supporterSectionRef.current.scrollIntoView({behavior: 'smooth'});\n\t\t}\n\t}\n\n\tscrollToConfigSection = () => {\n\t\tif (this.configSectionRef) {\n\t\t\tthis.configSectionRef.current.scrollIntoView({behavior: 'smooth'});\n\t\t}\n\t}\n\t\n\topenAnnouncementsCollapse = () => {\n\t\tthis.setState({\n\t\t\tisAnnouncementsCollapseOpen: !this.state.isAnnouncementsCollapseOpen,\n\t\t})\n\t}\n\n\tredirectToGoogleAuth = () => {\n\t\twindow.location = '/api/users/google-signin';\n\t}\n\n\tredirectToAppleAuth = () => {\n\t\twindow.location = '/api/users/apple-signin';\n\t}\n\n\tshowPassword = () => {\n\t\tthis.setState({\n\t\t\tshowPassword: !this.state.showPassword,\n\t\t});\n\t}\n\t\n\trenderPersonalDetailsCardForUser = (user, setUser) => {\n\t\tconst {classes} = this.props;\n\n\t\tlet isSubmitDisabled = !this.state.name;\n\t\t\n\t\treturn \n\t\t\t\n\t\t\t\t\t\tAccount\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\tsubheader='Your personal details.'\n\t\t\t\tavatar={ }\n\t\t\t/>\n\t\t\t \n\t\t\t\n\t\t\t\t\n\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\t\n\t\t\t\t
this.submitAccountDetailsUpdate(e, setUser)} disabled={isSubmitDisabled}>\n\t\t\t\t\tSave Details\n\t\t\t\t \n\t\t\t\n\t\t;\n\t}\n\t\n\trenderPersonalDetailsCardForGuest = (user, setUser) => {\n\t\tconst {classes} = this.props;\n\n\t\tlet isEmailInvalid = false;\n\t\tlet emailFieldHelperText = null;\n\t\tif (this.state.email && !Regexer.testEmail(this.state.email)) {\n\t\t\tisEmailInvalid = true;\n\t\t\temailFieldHelperText = 'Please enter a valid email'\n\t\t} else if (this.state.isEmailAlreadyTaken) {\n\t\t\tisEmailInvalid = true;\n\t\t\temailFieldHelperText = 'Email is already taken, please try another one';\n\t\t}\n\n\t\tlet isPasswordInvalid = false;\n\t\tlet passwordFieldHelperText = null;\n\t\tif (this.state.password && !Regexer.testPassword(this.state.password)) {\n\t\t\tisPasswordInvalid = true;\n\t\t\tpasswordFieldHelperText = 'Password must have a minimum length of 6 and contain a number.';\n\t\t}\n\n\t\tlet isSubmitDisabled = !this.state.name || !this.state.email || isEmailInvalid || !this.state.password || isPasswordInvalid;\n\n\t\treturn
\n\t\t\t\n\t\t\t\t\t\tAccount\n\t\t\t\t\t\n\t\t\t\t}\n\t\t\t\tsubheader='Your personal details.'\n\t\t\t\tavatar={ }\n\t\t\t/>\n\t\t\t\n\t\t\t \n\n\t\t\t\n\t\t\t\tRegister your account \n\t\t\t\tYou're currently using a guest account. To be able to sign back in to the app and avoid losing your data, register an account. No email verification required.\n\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t<>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t }>\n\t\t\t\t\t\t\t\tContinue with Google\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t }>\n\t\t\t\t\t\t\t\tContinue with Apple\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t \n\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\tOr Manually Register\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t>\n\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\n\t\t\t\t\t
this.submitConvertGuestToUser(e, setUser)} disabled={isSubmitDisabled}>\n\t\t\t\t\t\tRegister Account\n\t\t\t\t\t \n\t\t\t\t\n\t\t\n\t}\n\t\n\trenderSubmitFeedbackDialog = (user) => {\n\t\treturn
this.openSubmitFeedbackDialog(false)}\n\t\t\tTransitionComponent={Transition}\n\t\t>\n\t\t\tSubmit Feedback \n\t\t\t\n\t\t\t\t\n\t\t\t\t\tWant to help shape and improve the app? Submit bug reports, feature requests, or general feedback.\n\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t \n\n\t\t\t\t \n\t\t\t\t\t}\n\t\t\t\t\tlabel=\"If we have further questions, can we contact you by email?\"\n\t\t\t\t/>\n\t\t\t\t{this.state.shouldIncludeFeedbackEmail ?\n\t\t\t\t\t : null\n\t\t\t\t}\n\t\t\t\t\n\t\t\t \n\t\t\t\n\t\t\t\t this.openSubmitFeedbackDialog(false)}>\n\t\t\t\t\tClose\n\t\t\t\t \n\t\t\t\t this.submitFeedback(user)}>\n\t\t\t\t\tSubmit\n\t\t\t\t \n\t\t\t \n\t\t \n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tlet isPasswordInvalid = this.state.password && !Regexer.testPassword(this.state.password);\n\t\tlet isConfirmDifferent = this.state.confirmPassword && this.state.password !== this.state.confirmPassword;\n\t\tlet isPasswordSubmitDisabled = !this.state.password || isPasswordInvalid || !this.state.confirmPassword || isConfirmDifferent;\n\n\t\treturn (\n
\n\t\t\t\t{({user, setUser}) => {\n\t\t\t\t\tif (user) {\n\t\t\t\t\t\treturn <>\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{this.renderSubmitFeedbackDialog(user)}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t this.openSupporterDialog(false)}\n\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tstyle={{paddingBottom: 55}}\n\t\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\tAnnouncements\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tsubheader='See the latest new features, changes, and announcements for the app.'\n\t\t\t\t\t\t\t\t\tavatar={\n\t\t\t\t\t\t\t\t\t\tuser.hasNotifications ?\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t20 October 2024: \n\t\t\t\t\t\t\t\t\t\t• Include goals overview in home page. \n\t\t\t\t\t\t\t\t\t\t• Include Start Workout action bar in Routines page (also allowing to log past workouts). \n\t\t\t\t\t\t\t\t\t\t• ☕ Consider purchasing a Supporter's Badge to support the\n\t\t\t\t\t\t\t\t\t\tapp's development and help cover server costs. Learn more: \n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t21 January 2024: \n\t\t\t\t\t\t\t\t\t\t\t• 1RM graphs now included in graphs tab (Brzycki formula). \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t9 July 2023: \n\t\t\t\t\t\t\t\t\t\t\t• You can export your exercise logs as a CSV. Option is in the Filter menu (bottom left) of any routine exercise logs page. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t3 July 2023: \n\t\t\t\t\t\t\t\t\t\t\t• The timer now has a settings icon where you can set whether setting a timer value is global (i.e. applies to all exercises). \n\t\t\t\t\t\t\t\t\t\t\t• RPE values (Rating of perceived exertion) are now included in the 'Max Reps per Weight' graph. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t25 June 2023: \n\t\t\t\t\t\t\t\t\t\t\t• Added a workout statistics section in the homepage. \n\t\t\t\t\t\t\t\t\t\t\t• Overall performance and UI improvements. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t20 June 2023: \n\t\t\t\t\t\t\t\t\t\t\t• Performance improvements to the logs table and graphs. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t6 April 2023: \n\t\t\t\t\t\t\t\t\t\t\t• Bug fix for app breaking when timer field was opened. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t6 Nov 2022: \n\t\t\t\t\t\t\t\t\t\t\t• You can now set custom colors for routines, exercises, and measurements. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t25 Jul 2022: \n\t\t\t\t\t\t\t\t\t\t\t• The quick button behaviour in the logger is now such that it first logs your set and only then starts the timer. \n\t\t\t\t\t\t\t\t\t\t\t• Added a quick timer start button in the logger. \n\t\t\t\t\t\t\t\t\t\t\t• Lots of internal performance and security improvements. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t22 Jun 2022: \n\n\t\t\t\t\t\t\t\t\t\t\t• You can now add comments to your exercise plan logs. \n\t\t\t\t\t\t\t\t\t\t\t• The adjusting weights + - buttons values were updated. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t2 Apr 2022: \n\n\t\t\t\t\t\t\t\t\t\t\t• You can now enter a muscle breakdown per exercise (in the exercises page). With this, in the workout detail page you can see a muscle breakdown graph of your current/past workout. \n\t\t\t\t\t\t\t\t\t\t\t• Performance and stability improvements. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t19 Feb 2022: \n\n\t\t\t\t\t\t\t\t\t\t\t• You can now archive exercises in routines. Use this to hide exercises you no longer want to see during your workout routines but still want access to. \n\t\t\t\t\t\t\t\t\t\t\t• Added option to show all logs for an exercise, even if the exercise is part of multiple routines. Do this in the time filtering menu on the bottom left of the exercise logger. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t26 Dec 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Added possibility to add past workouts. Do this by visiting a routine page and\n\t\t\t\t\t\t\t\t\t\t\tclicking the 3-dot menu next to the Start Workout toolbar. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t23 Dec 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• (Internal change) Upgraded the theme/design engine to the latest version.\n\t\t\t\t\t\t\t\t\t\t\tIf any bugs are found, please report them immediately. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t16 Dec 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Routine and measurement cards can now be re-ordered. Hold and drag the\n\t\t\t\t\t\t\t\t\t\t\tdouble arrow icon in the bottom right of the cards to do this. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t11 Dec 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Superset exercises now automatically switch to the next exercise in the\n\t\t\t\t\t\t\t\t\t\t\tsuperset when you log a set. \n\t\t\t\t\t\t\t\t\t\t\t• Lots of under-the-hood improvements and upgrades. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t14 Nov 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Added ability to move an exercise to another routine while keeping your\n\t\t\t\t\t\t\t\t\t\t\tlogs. Do this via the exercise card menus in your routine pages. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t21 Oct 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Implemented exercise supersets. Add them via the exercise card menus in\n\t\t\t\t\t\t\t\t\t\t\tyour routine pages. \n\t\t\t\t\t\t\t\t\t\t\t• Overall UI and performance improvements. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t26 Aug 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• You can now edit the workout times and moods of existing workouts. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t6 Aug 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Added a workout planner feature in the exercise logger to remind you of\n\t\t\t\t\t\t\t\t\t\t\twhich and how many sets to do during your workouts. Noticed any bugs or have\n\t\t\t\t\t\t\t\t\t\t\tsuggestions? Go to feedback section: \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t6 Aug 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Allow exercises to be added more than once in a routine. \n\t\t\t\t\t\t\t\t\t\t\t• General UI fixes and improvements. \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t23 Jun 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Overhaul of the goals system: Rebuilt the system from the ground up with\n\t\t\t\t\t\t\t\t\t\t\tlots of improvements. The goals are now part of your routine-exercises (see\n\t\t\t\t\t\t\t\t\t\t\tGoals tab in the logger). \n\t\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t12 Jun 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Overhaul of the logger: improved design and added dedicated statistics\n\t\t\t\t\t\t\t\t\t\t\tsection. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t2 Jun 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• When completing a workout, you can now specify a mood and end time.\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t27 May 2021: \n\n\t\t\t\t\t\t\t\t\t\t\t• Added dark mode option in the Settings page. \n\t\t\t\t\t\t\t\t\t\t\t• Added RPE/RIR logging. \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t{this.state.isAnnouncementsCollapseOpen ? :\n\t\t\t\t\t\t\t\t\t\t\t\t }\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{user.email ? \n\t\t\t\t\t\t\t\tthis.renderPersonalDetailsCardForUser(user, setUser) \n\t\t\t\t\t\t\t\t: \n\t\t\t\t\t\t\t\tthis.renderPersonalDetailsCardForGuest(user, setUser)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\tConfigurations\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tsubheader='Tweak the app to your taste.'\n\t\t\t\t\t\t\t\t\tavatar={ }\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tUnit System \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t this.submitPreferredUnitSystemId(e, user, setUser)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t } label={Metric (kg, km) } />\n\t\t\t\t\t\t\t\t\t\t\t\t\t } label={Imperial (lbs, miles) } />\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tDark Mode \n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t this.submitHasDarkModeEnabled(e, user, setUser)}>\n\t\t\t\t\t\t\t\t\t\t\t\t\t} label={Enabled } />\n\t\t\t\t\t\t\t\t\t\t\t\t\t} label={Disabled } />\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\tOther\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tsubheader='Submit feedback and support development.'\n\t\t\t\t\t\t\t\t\tavatar={ }\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t this.openSubmitFeedbackDialog(true)}>\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\tSubmit Feedback} secondary='Submit bug reports, feature requests, or general feedback.'/>\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{user.email &&\n\t\t\t\t\t\t\t\t\t\t\t this.openSupporterDialog(true)} ref={this.supporterSectionRef}>\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\t{user.isSupporter ?\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t :\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\t\t{user.isSupporter ?\n\t\t\t\t\t\t\t\t\t\t\t\t\tYou are a supporter!} secondary='See your Supporter Badge and the Supporters List. Donate again if you wish.'/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t:\n\t\t\t\t\t\t\t\t\t\t\t\t\tBecome a supporter } secondary='Purchase a supporter badge and get listed in the Supporters List.'/>\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t{user.email &&\n\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t \n\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\t\tSecurity\n\t\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tsubheader='Change your password.'\n\t\t\t\t\t\t\t\t\t\t\tavatar={ }\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t\t
this.submitPasswordUpdate(e, setUser)}>\n\t\t\t\t\t\t\t\t\t\t\t\tUpdate Password\n\t\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t\t this.logOut(e, setUser)} size='large'>\n\t\t\t\t\t\t\t\t\tLog out\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t
\n\t\t\t\t\t\t>;\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\n );\n\t}\n}\n\nSettings.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(Settings));","import React from 'react';\nimport {useLocation, useNavigate} from \"react-router\";\n\nconst Redirect = () => {\n\tlet location = useLocation();\n\tlet navigate = useNavigate();\n\n\tlet redirect = (new URLSearchParams(location.search)).get('link');\n\t\n\tReact.useEffect(() => {\n\t\tnavigate(redirect ? redirect : '/');\n\t});\n\t\n\treturn null;\n}\n\nexport default Redirect;\n","import React, {PureComponent} from 'react'\nimport withStyles from '@mui/styles/withStyles';\nimport DialogTitle from \"@mui/material/DialogTitle\";\nimport IconButton from \"@mui/material/IconButton\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport {Dialog, DialogContent, Grid} from \"@mui/material\";\nimport InputLabel from \"@mui/material/InputLabel\";\nimport Select from \"@mui/material/Select\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport FormControl from \"@mui/material/FormControl\";\nimport Input from \"@mui/material/Input\";\nimport Button from \"@mui/material/Button\";\nimport DialogActions from \"@mui/material/DialogActions\";\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {ExerciseType} from \"../../../core/models/ExerciseType\";\n\nconst useStyles = theme => ({\n\tformControl: {\n\t\tmargin: theme.spacing(1),\n\t\tminWidth: 200,\n\t},\n});\n\nclass ExampleExerciseNewOrEditFormPopUp extends PureComponent {\n\n\t/**\n\t * @param props Containing:\n\t * 'closeSelfFunc': The function to call to close this popup.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The example-exercise to edit, if any. If this is given, the category is ignored.\n\t\t\t */\n\t\t\texercise: props.exercise,\n\t\t\t/**\n\t\t\t * The category to use to add the exercise (i.e. which category does this example-exercise belong to?).\n\t\t\t */\n\t\t\tcategory: props.category,\n\t\t\t/**\n\t\t\t * The name of the new example exercise.\n\t\t\t */\n\t\t\tname: props.exercise ? props.exercise.name : '',\n\t\t\t/**\n\t\t\t * What type of exercise is it?\n\t\t\t */\n\t\t\tselectedExerciseTypeId: 1,\n\t\t};\n\t}\n\t\n\tcomponentWillReceiveProps(nextProps, nextContext) {\n\t\tthis.setState({\n\t\t\tcategory: nextProps.category,\n\t\t\texercise: nextProps.exercise,\n\t\t\t\n\t\t\tname: nextProps.exercise ? nextProps.exercise.name : '',\n\t\t\tselectedExerciseTypeId: nextProps.exercise ? nextProps.exercise.exerciseTypeId : 1,\n\t\t\t\n\t\t\t...this.getMusclesStateFromExercise(nextProps.exercise)\n\t\t});\n\t}\n\t\n\tgetMusclesStateFromExercise = (exercise) => {\n\t\tif (!exercise) {\n\t\t\treturn {};\n\t\t}\n\t\t\n\t\tlet state = {};\n\n\t\tlet i = 0;\n\t\tfor (const exampleMuscle of exercise.exampleMuscles) {\n\t\t\tstate[this.getStateKeyForMuscleId(i)] = exampleMuscle.muscle.id;\n\t\t\tstate[this.getStateKeyForMusclePercentage(i)] = exampleMuscle.percentage;\n\t\t\ti++;\n\t\t}\n\t\t\n\t\treturn state;\n\t}\n\n\tupdateStateInputValues = (event) => {\n\t\tthis.setState({\n\t\t\t[event.target.name]: event.target.value,\n\t\t});\n\t}\n\n\tsubmit = async (event) => {\n\t\tevent.preventDefault();\n\t\t\n\t\tlet muscles = [];\n\t\t\n\t\t[0, 1, 2, 3, 4].forEach(num => {\n\t\t\tif (this.state[`musclePercentage${num}`] && this.state[`musclePercentage${num}`] > 0) {\n\t\t\t\tmuscles.push({\n\t\t\t\t\tmuscleId: this.state[this.getStateKeyForMuscleId(num)] ?? 1, // Fallback to Triceps,\n\t\t\t\t\tpercentage: this.state[this.getStateKeyForMusclePercentage(num)],\n\t\t\t\t});\n\t\t\t}\n\t\t})\n\n\t\tlet exampleExerciseToSave = {\n\t\t\tname: this.state.name,\n\t\t\texerciseTypeId: this.state.selectedExerciseTypeId,\n\t\t\texampleMuscles: muscles,\n\t\t};\n\n\t\tif (this.state.exercise) { // When updating\n\t\t\texampleExerciseToSave['id']= this.state.exercise.id;\n\t\t}\n\n\t\tif (this.state.category) { // When new (so category cannot change).\n\t\t\texampleExerciseToSave['exampleExerciseCategoryId']= this.state.category.id;\n\t\t}\n\n\t\tlet res = await fetch(UrlBuilder.exercise.exerciseFormExamplesApi(), {\n\t\t\tmethod: this.state.category ? 'POST' : 'PUT',\n\t\t\theaders: {\n\t\t\t\t'Accept': 'application/json',\n\t\t\t\t'Content-Type': 'application/json'\n\t\t\t},\n\t\t\tbody: JSON.stringify(exampleExerciseToSave)\n\t\t});\n\n\t\tthis.props.closeSelfFunc(true);\n\t}\n\n\tgetStateKeyForMuscleId = (num) => {\n\t\treturn `muscle${num}`;\n\t}\n\n\tgetStateKeyForMusclePercentage = (num) => {\n\t\treturn `musclePercentage${num}`;\n\t}\n\t\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\tif (!this.state.category && !this.state.exercise) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn
this.props.closeSelfFunc(false)}\n\t\t\tcloseAfterTransition\n\t\t>\n\t\t\t\n\t\t\t\t{this.state.category ?\n\t\t\t\t\t<>Add exercise for {this.state.category.name}>\n\t\t\t\t\t:\n\t\t\t\t\t<>Edit exercise {this.state.exercise.name}>\n\t\t\t\t}\n\t\t\t\t\n\n\t\t\t\t this.props.closeSelfFunc(false)}>\n\t\t\t\t\t \n\t\t\t\t \n\t\t\t \n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tName \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\tType\n\t\t\t\t\t \n\t\t\t\t\t\n\t\t\t\t\t\tWeight & Reps \n\t\t\t\t\t\tWeight & Time \n\t\t\t\t\t\tReps \n\t\t\t\t\t\tDistance & Time \n\t\t\t\t\t\tTime \n\t\t\t\t\t \n\t\t\t\t \n\t\t\t\t\n\t\t\t\t \n\t\t\t\t \n\n\t\t\t\t\n\t\t\t\t\t{[0, 1, 2, 3, 4].map(num => {\n\t\t\t\t\t\tconst muscle = this.getStateKeyForMuscleId(num);\n\t\t\t\t\t\tconst musclePercentage = this.getStateKeyForMusclePercentage(num)\n\t\t\t\t\t\t\n\t\t\t\t\t\treturn \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\tTriceps \n\t\t\t\t\t\t\t\t\tBiceps \n\t\t\t\t\t\t\t\t\tForearms \n\t\t\t\t\t\t\t\t\tShoulders \n\t\t\t\t\t\t\t\t\tChest \n\t\t\t\t\t\t\t\t\tLats \n\t\t\t\t\t\t\t\t\tTraps \n\t\t\t\t\t\t\t\t\tQuads \n\t\t\t\t\t\t\t\t\tHamstrings \n\t\t\t\t\t\t\t\t\tCalves \n\t\t\t\t\t\t\t\t\tGlutes \n\t\t\t\t\t\t\t\t\tCore \n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t})}\n\t\t\t\t \n\t\t\t \n\n\t\t\t\n\t\t\t\t this.props.closeSelfFunc(false)}>\n\t\t\t\t\tClose\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\tSave\n\t\t\t\t \n\t\t\t \n\t\t ;\n\t}\n}\n\nexport default withStyles(useStyles)(ExampleExerciseNewOrEditFormPopUp);","import React, {PureComponent} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport {UrlBuilder} from \"../../../core/url/UrlBuilder\";\nimport {Collapse, List, ListItem, ListItemSecondaryAction, ListItemText, TextField} from \"@mui/material\";\nimport Typography from \"@mui/material/Typography\";\nimport Button from \"@mui/material/Button\";\nimport ExampleExerciseNewOrEditFormPopUp from \"../partials/ExampleExerciseNewOrEditFormPopUp\";\nimport Box from \"@mui/material/Box\";\nimport Divider from \"@mui/material/Divider\";\n\nconst useStyles = theme => ({\n});\n\n\nclass Admin extends PureComponent {\n\n\tconstructor(props) {\n\t\tsuper(props);\n\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The exercise categories to load in the fetch (each category has exercises).\n\t\t\t */\n\t\t\texampleExerciseCategories: [],\n\t\t\t/**\n\t\t\t * The category to add an exercise for.\n\t\t\t */\n\t\t\tcategoryToAddExerciseFor: null,\n\t\t\t/**\n\t\t\t * The exercise to edit, if any.\n\t\t\t */\n\t\t\texerciseToEdit: null,\n\t\t\t/**\n\t\t\t * Latest workouts.\n\t\t\t */\n\t\t\tlatestWorkouts: [],\n\t\t\t/**\n\t\t\t * The days ago.\n\t\t\t */\n\t\t\tdaysAgo: null,\n\t\t};\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.refresh();\n\t}\n\n\trefresh = () => {\n\t\tthis.fetchExerciseFormExamples();\n\t}\n\n\tfetchExerciseFormExamples = async () => {\n\t\tawait fetch(\n\t\t\tUrlBuilder.exercise.allExerciseFormExamplesApi()\n\t\t)\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.setState({\n\t\t\t\t\texampleExerciseCategories: response['data'] ?? [],\n\t\t\t\t});\n\t\t\t});\n\t}\n\n\tfetchLatestWorkouts = async () => {\n\t\tif (this.state.daysAgo === null) {\n\t\t\treturn;\n\t\t}\n\t\t\n\t\tawait fetch(\n\t\t\tUrlBuilder.workout.workoutsApi(null, this.state.daysAgo)\n\t\t)\n\t\t\t.then(response => response.json())\n\t\t\t.then(response => {\n\t\t\t\tthis.setState({\n\t\t\t\t\tlatestWorkouts: response['data'] ?? [],\n\t\t\t\t});\n\t\t\t});\n\t}\n\t\n\topenCollapse = (categoryName) => {\n\t\tthis.setState({\n\t\t\t[categoryName]: !this.state[categoryName], // make it the opposite of what it was.\n\t\t});\n\t}\n\n\tclosePopup = (shouldRefresh = false) => {\n\t\tthis.setState({\n\t\t\tcategoryToAddExerciseFor: null,\n\t\t\texerciseToEdit: null,\n\t\t});\n\t\t\n\t\tif (shouldRefresh) {\n\t\t\tthis.refresh();\n\t\t}\n\t}\n\t\n\tsetCategoryToAddExerciseFor = (e, categoryToAddExerciseFor) => {\n\t\te.stopPropagation();\n\t\t\n\t\tthis.setState({\n\t\t\tcategoryToAddExerciseFor: categoryToAddExerciseFor,\n\t\t});\n\t}\n\t\n\tsetExerciseToEdit = (exercise) => {\n\t\tthis.setState({\n\t\t\texerciseToEdit: exercise,\n\t\t});\n\t}\n\n\thandleInputChange = event => {\n\t\tconst value = event.target.value;\n\t\tthis.setState({ daysAgo: value });\n\t};\n\t\n\tgetDurationInHoursAndMinutes = (start, end) => {\n\t\tconst startTime = new Date(start);\n\t\tconst endTime = new Date(end);\n\t\tconst durationInMs = endTime - startTime;\n\n\t\tconst hours = Math.floor(durationInMs / (1000 * 60 * 60));\n\t\tconst minutes = Math.floor((durationInMs % (1000 * 60 * 60)) / (1000 * 60));\n\n\t\treturn `${hours}h ${minutes}m`;\n\t};\n\t\n\trenderLatestWorkouts = () => {\n\t\tconst workouts = this.state.latestWorkouts;\n\n\t\treturn
\n\t\t\t\n\t\t\t\t{workouts.slice().reverse().map((workout, index) => (\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\t{`${workouts.length - index}. ${workout.user.name}`} \n\t\t\t\t\t\t\t\t\t\t{`Email: ${workout.user.email}`} \n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsecondary={\n\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t{`Started: ${new Date(workout.dateTimeStartedUtc).toLocaleString()}`} \n\t\t\t\t\t\t\t\t\t\t{workout.dateTimeEndedUtc && (\n\t\t\t\t\t\t\t\t\t\t\t{`Ended: ${new Date(workout.dateTimeEndedUtc).toLocaleString()}`} \n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t{workout.dateTimeEndedUtc && (\n\t\t\t\t\t\t\t\t\t\t\t{`Duration: ${this.getDurationInHoursAndMinutes(workout.dateTimeStartedUtc, workout.dateTimeEndedUtc)}`} \n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t \n\t\t\t\t\t\t{index !== workouts.length - 1 && }\n\t\t\t\t\t \n\t\t\t\t))}\n\t\t\t
\n\t\t \n\t}\n\n\trender() {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t
this.closePopup(shouldRefresh)}\n\t\t\t\t/>\n\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\tSubmit\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t{this.renderLatestWorkouts()}\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\tCategories\n\t\t\t\t \n\t\t\t\t\n\t\t\t\t\t{this.state.exampleExerciseCategories.map(category => \n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t this.openCollapse(category.name)}>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{category.name}\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t this.setCategoryToAddExerciseFor(e, category)}>\n\t\t\t\t\t\t\t\t\tADD NEW\n\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t{category.exampleExercises.map(exercise =>\n\t\t\t\t\t\t\t\t\t\t this.setExerciseToEdit(exercise)}>\n\t\t\t\t\t\t\t\t\t\t\t{exercise.name}\n\t\t\t\t\t\t\t\t\t\t \n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t
\n\t\t\t\t\t\t\t \n\t\t\t\t\t\t>\n\t\t\t\t\t)}\n\t\t\t\t
\n\t\t\t>\n\t\t);\n\t}\n}\n\nexport default withStyles(useStyles)(Admin);","import React, {PureComponent} from 'react';\nimport withStyles from '@mui/styles/withStyles';\nimport {PageDataContext} from \"../../../core/PageDataContext\";\nimport {withRouter} from \"../../../hooks/withRouter\";\nimport {Container} from \"@mui/material\";\n\nconst useStyles = theme => ({});\n\nclass PrivacyPolicyPage extends PureComponent {\n\n\t/**\n\t * @param props No params needed.\n\t */\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {};\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.context.setPageData({\n\t\t\ttitle: \"Privacy Policy\",\n\t\t});\n\t}\n\n\trender() {\n\t\tconst {classes} = this.props;\n\n\t\treturn \n\t\t\tMy Work in Progress Privacy Policy \n\t\t\tThis app, My Work in Progress, is a workout tracking app provided as a free service. This Service is provided by\n\t\t\tMy Work in Progress at no cost and is intended for use as\n\t\t\tis.\n\t\t\n\t\t\tThis page is used to inform visitors regarding my\n\t\t\tpolicies with the collection, use, and disclosure of Personal\n\t\t\tInformation if anyone decided to use my Service.\n\t\t
\n\t\t\tIf you choose to use my Service, then you agree to\n\t\t\tthe collection and use of information in relation to this\n\t\t\tpolicy. The Personal Information that I collect is\n\t\t\tused for providing and improving the Service. I will not use or share your information with\n\t\t\tanyone except as described in this Privacy Policy.\n\t\t
\n\t\t\tInformation Collection and Use
\n\t\t\tFor a better experience, while using our Service, I\n\t\t\tmay require you to provide us with certain personally\n\t\t\tidentifiable information, including but not limited to email and your name/nickname.\n\t\t\tThe email is used with the sole purpose to create an account for you on the web, so that you may access your\n\t\t\tworkout data on the internet with said account, on any device through https://myworkinprogress.app . Your email\n\t\t\tdoes not require verification.\n\t\t
\n\t\t\tLog Data
\n\t\t\tI want to inform you that whenever you\n\t\t\tuse my Service, in a case of an error in the app\n\t\t\tI collect data and information (through third-party\n\t\t\tproducts) on my server called Log Data. This Log Data may\n\t\t\tinclude information such as your device Internet Protocol\n\t\t\t(“IP”) address, device name, operating system version, the\n\t\t\tconfiguration of the app when utilizing my Service,\n\t\t\tthe time and date of your use of the Service, and other\n\t\t\tstatistics.\n\t\t
Cookies
\n\t\t\tCookies are files with a small amount of data that are\n\t\t\tcommonly used as anonymous unique identifiers. These are sent\n\t\t\tto your browser from the websites that you visit and are\n\t\t\tstored on your device's internal memory.\n\t\t
\n\t\t\tThis Service requires the use of “cookies”. They are required to keep you signed in\n\t\t\tand provide a personalized experience. If you\n\t\t\tchoose to refuse our cookies, you may not be able to use the service.\n\t\t
\n\t\t\tSecurity
\n\t\t\tI value your trust in providing us your\n\t\t\tPersonal Information, thus we are striving to use commercially\n\t\t\tacceptable means of protecting it. But remember that no method\n\t\t\tof transmission over the internet, or method of electronic\n\t\t\tstorage is 100% secure and reliable, and I cannot\n\t\t\tguarantee its absolute security.\n\t\t
\n\t\t\tChanges to This Privacy Policy
\n\t\t\tI may update our Privacy Policy from\n\t\t\ttime to time. Thus, you are advised to review this page\n\t\t\tperiodically for any changes. I will\n\t\t\tnotify you of any changes by posting the new Privacy Policy on\n\t\t\tthis page.\n\t\t
Contact Us
\n\t\t\tIf you have any questions or suggestions about my\n\t\t\tPrivacy Policy, do not hesitate to contact me at myworkinprogress.app@gmail.com.\n\t\t
\n\t\t \n\t\t \n\t}\n}\n\nPrivacyPolicyPage.contextType = PageDataContext;\n\nexport default withStyles(useStyles)(withRouter(PrivacyPolicyPage));","import React, {PureComponent} from 'react';\nimport {Route, Routes} from 'react-router-dom';\n\nimport {UrlBuilder} from \"./core/url/UrlBuilder\";\nimport {UserContext} from \"./core/UserContext\";\n\nimport Container from \"@mui/material/Container\";\nimport {ThemeProvider, StyledEngineProvider, keyframes} from '@mui/material/styles'\nimport { styled } from '@mui/system';\nimport withStyles from '@mui/styles/withStyles';\nimport Backdrop from \"@mui/material/Backdrop\";\nimport {PageDataContext} from \"./core/PageDataContext\";\n\n// We purposefully load these non-lazily because we expect them to be frequently accessed.\nimport Home from \"./components/home/Home\";\nimport BottomNavMenu from \"./components/core/BottomNavMenu\";\nimport TopNavMenu from \"./components/core/TopNavMenu\";\nimport RoutinesOverview from \"./components/routine/pages/RoutinesOverview\";\nimport ExercisesOverview from \"./components/exercise/pages/ExercisesOverview\";\nimport RoutineDetail from \"./components/routine/pages/RoutineDetail\";\nimport theme from \"./theme\";\nimport workoutTheme from \"./workoutTheme\";\nimport darkTheme from \"./darkTheme\";\nimport CssBaseline from \"@mui/material/CssBaseline\";\nimport WorkoutCompleteBloop from \"./components/workout/WorkoutCompleteBloop\";\nimport Signin from \"./components/user/Signin\";\nimport Signup from \"./components/user/Signup\";\nimport WorkoutDetail from \"./components/workout/pages/WorkoutDetail\";\nimport MeOverview from \"./components/me/pages/MeOverview\";\nimport {UserLocalStorage} from \"./core/storage/UserLocalStorage\";\nimport ScrollToTop from \"./core/util/ScrollToTop\";\nimport WIPIcon from \"./res/img/icon.png\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Grid from \"@mui/material/Grid\";\nimport {bounceInDown} from 'react-animations';\nimport Settings from \"./components/user/Settings\";\nimport Box from \"@mui/material/Box\";\nimport * as Sentry from \"@sentry/react\";\nimport Typography from \"@mui/material/Typography\";\nimport RefreshIcon from '@mui/icons-material/Refresh';\nimport Redirect from \"./components/core/Redirect\";\nimport Admin from \"./components/admin/pages/Admin\";\nimport PrivacyPolicyPage from \"./components/help/pages/PrivacyPolicyPage\";\n\nconst bounceAnimation = keyframes`${bounceInDown}`;\nconst PulseDiv = styled('div')(({ theme }) => ({\n\tanimation: `1.2s ${bounceAnimation}`\n}));\n\nconst useStyles = theme => ({\n\thidden: {\n\t\tdisplay: 'none',\n\t}\n});\n\nclass App extends PureComponent {\n\t\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.state = {\n\t\t\t/**\n\t\t\t * The user to be loaded universally in the app (through the provider/consumer).\n\t\t\t */\n\t\t\tuser: null,\n\t\t\t/**\n\t\t\t * Page meta data loaded universally in the app (through the provider/consumer).\n\t\t\t */\n\t\t\tpageData : {\n\t\t\t\ttitle: \"My Work in Progress\",\n\t\t\t},\n\t\t\t/**\n\t\t\t * Initialize as false. This is usually triggered by the user himself manually.\n\t\t\t */\n\t\t\tisWorkoutBloopOpen: false, \n\t\t\t/**\n\t\t\t * Initially we do set as loading, as we fetch the user until we have mounted.\n\t\t\t */\n\t\t\tisLoading: true,\n\t\t\t/**\n\t\t\t * Show the backdrop?\n\t\t\t */\n\t\t\tshowBackdrop: true,\n\t\t};\n\t}\n\t\n\tcomponentDidCatch(error, errorInfo) {\n\t\tlocalStorage.clear(); // Here we guess that something went wrong with the localStorage situation.\n\t}\n\n\tcomponentDidMount() {\n\t\tsetTimeout(() => this.closeBackdrop(), 1000);\n\t\tthis.initializeUser();\n\t}\n\t\n\tcloseBackdrop = () => {\n\t\tthis.setState({\n\t\t\tshowBackdrop: false,\n\t\t});\n\t}\n\n\tinitializeUser = async () => {\n\t\tlet user = await fetch('/api/users/current')\n\t\t\t.then(response => response.status === 200 ? response.json() : null)\n\t\t\t.catch(res => {\n\t\t\t\treturn null;\n\t\t\t})\n\n\t\tthis.setUser(user);\n\t}\n\n\tsetUser = (user) => {\n\t\tif (user) {\n\t\t\tUserLocalStorage.set('user', user)\n\t\t\tif (user.activeWorkout) {\n\t\t\t\tthis.updateMetaThemeColor(user.hasDarkModeEnabled, true);\n\t\t\t\tthis.setState({\n\t\t\t\t\tuser: user,\n\t\t\t\t\tisLoading: false,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.updateMetaThemeColor(user.hasDarkModeEnabled, false);\n\t\t\t\tthis.setState({\n\t\t\t\t\tuser: user,\n\t\t\t\t\tisLoading: false,\n\t\t\t\t\tisWorkoutBloopOpen: false,\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tlocalStorage.clear();\n\t\t\tthis.updateMetaThemeColor(false, false);\n\t\t\tthis.setState({\n\t\t\t\tuser: null,\n\t\t\t\tisLoading: false,\n\t\t\t\tisWorkoutBloopOpen: false,\n\t\t\t});\n\t\t}\n\t\tthis.forceUpdate();\n\t}\n\t\n\tupdatePageData = (data) => {\n\t\tthis.setState({\n\t\t\tpageData: data,\n\t\t});\n\t}\n\t\n\topenActiveWorkoutBloop = (isOpen) => {\n\t\tif (isOpen && this.isWorkoutActive()) {\n\t\t\tthis.setState({\n\t\t\t\tisWorkoutBloopOpen: true\n\t\t\t});\n\t\t} else {\n\t\t\tthis.setState({\n\t\t\t\tisWorkoutBloopOpen: false\n\t\t\t});\n\t\t}\n\t}\n\t\n\tupdateMetaThemeColor = (isDarkModeEnabled, isWorkoutActive) => {\n\t\tlet metaThemeColor = document.querySelector(\"meta[name=theme-color]\");\n\t\tmetaThemeColor.setAttribute(\"content\",\n\t\t\tisDarkModeEnabled ? darkTheme.palette.primary.main : (isWorkoutActive ? workoutTheme.palette.primary.main : theme.palette.primary.main)\n\t\t);\n\t}\n\t\n\tisWorkoutActive = () => {\n\t\treturn this.state.user && this.state.user.activeWorkout;\n\t}\n\t\n\tisDarkModeEnabled = () => {\n\t\treturn this.state.user && this.state.user.hasDarkModeEnabled;\n\t}\n\n\trenderErrorPage = () => {\n\t\tconst {classes} = this.props;\n\n\t\tconst refresh = () => {\n\t\t\twindow.location.href = '/';\n\t\t}\n\n\t\treturn (\n \n \n \n \n \n \n \n \n Something went wrong.\n \n \n \n \n \n );\n\t}\n\t\n\tgetErrorDialogOptions = () => {\n\t\treturn {\n\t\t\tuser: {\n\t\t\t\temail: this.state.user ? this.state.user.email : null, \n\t\t\t\tname: this.state.user ? this.state.user.name : null\n\t\t\t},\n\t\t}\n\t}\n\n\thandleErrors = () => {\n\t\tlocalStorage.clear(); // Here we guess that something went wrong with the localStorage situation.\n\t}\n\n\trenderApp = () => {\n\t\tconst {classes} = this.props;\n\n\t\treturn (\n \n \n \n \n \n \n \n\n \n\n \n \n } />\n\n } />\n\n } />\n } />\n\n } />\n } />\n } />\n \n } />\n \n } />\n \n } />\n\n } />\n\t \n } />\n \n \n \n \n\n this.openActiveWorkoutBloop(false)}\n />\n
\n \n \n \n \n );\n\t}\n\t\n\trender() {\n\t\treturn (\n \n\t\t\t\t\n \n theme.zIndex.drawer + 1,\n\t backgroundColor: 'rgb(255,255,255)',\n\t }}\n >\n \n \n \n \n \n \n \n \n\n \n \n \n \n \n\t\t\t\n\t\t\t\t{!this.state.isLoading ? this.renderApp() : null}\n \n );\n\t}\n}\n\nexport default withStyles(useStyles)(App)","import React from 'react';\nimport ReactDOM from 'react-dom';\nimport { BrowserRouter } from 'react-router-dom';\nimport App from './App';\nimport * as Sentry from \"@sentry/react\";\n\nimport 'fontsource-roboto';\n\nconst baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');\nconst rootElement = document.getElementById('root');\n\nif (process.env.NODE_ENV && process.env.NODE_ENV === 'production') {\n Sentry.init({\n dsn: \"https://733310e04a374ebe8e8f3fa253ce4a99@o573578.ingest.sentry.io/5724185\",\n environment: 'production'\n });\n}\n\n\nReactDOM.render(\n \n \n ,\n rootElement\n);\n\n// Register the service worker.\nif ('serviceWorker' in navigator) {\n window.addEventListener('load', () => {\n navigator.serviceWorker\n .register('service_worker.js')\n .then(reg => console.log('Service Worker: Registered (Pages)'))\n .catch(err => console.log(`Service Worker: Error: ${err}`));\n });\n}\n"],"sourceRoot":""}