@@ -4,19 +4,36 @@ import { Camera, Permissions, FileSystem, CameraObject } from 'expo';
import { SafeAreaView } from 'react-navigation';
import sharedStyles from '../sharedStyles';
import Colors from '../../Colors';
import Preview from './Preview';
import { STORAGE_DIR, TEMP_PATH } from './constants';
import { changeImage } from '../../redux/actions';
import { NavigationProps } from '../../navigation/rootNavigation';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Dispatch } from '../../redux';

const STORAGE_DIR = 'photos';
const CAMERA_BACK = 'back';
const CAMERA_FRONT = 'front';

interface StateProps {
workoutId: string;
}

interface ActionProps {
readonly changeImage: (uri: string) => void;
}

type OwnProps = NavigationProps;

type Props = ActionProps & StateProps & OwnProps;

interface LocalState {
type: string;
hasCameraPermission: null | boolean;
// photoId: number;
showImage: boolean;
}

export default class CameraComponent extends React.Component<{}, LocalState> {
class CameraComponent extends React.Component<Props, LocalState> {
camera: CameraObject | null;

constructor(props: any) {
@@ -26,7 +43,6 @@ export default class CameraComponent extends React.Component<{}, LocalState> {
this.state = {
type: CAMERA_BACK,
hasCameraPermission: null,
// photoId: 1,
showImage: false,
};
}
@@ -69,74 +85,113 @@ export default class CameraComponent extends React.Component<{}, LocalState> {

takePicture = async () => {
if (this.camera) {
const imageDestination = `${FileSystem.documentDirectory}${TEMP_PATH}`;
this.camera.takePictureAsync({}).then(data => {
FileSystem.moveAsync({
return FileSystem.moveAsync({
from: data.uri,
// TODO - need to move image to permanent storage once it's accepted
to: `${FileSystem.documentDirectory}${STORAGE_DIR}/temp`,
to: imageDestination,
}).then(() => {
Vibration.vibrate(100, false);
this.setState({
// photoId: this.state.photoId + 1,
showImage: true,
});
});
});
}
};

acceptPhoto = (tempUri: string) => {
const now = new Date().getMilliseconds();
const finalDestination = `${
FileSystem.documentDirectory
}${STORAGE_DIR}/${now}.jpg`;
FileSystem.moveAsync({
from: tempUri,
to: finalDestination,
}).then(() => {
this.props.changeImage(finalDestination);
});
};

declinePhoto = () => {
this.setState({ showImage: false });
};

renderCamera = () => {
return (
<Camera
ref={(ref: any) => {
this.camera = ref;
}}
style={{ flex: 1 }}
type={this.state.type}>
<SafeAreaView style={sharedStyles.safeArea}>
<View
style={{
backgroundColor: 'transparent',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
margin: 16,
}}>
<TouchableOpacity onPress={this.takePicture}>
<Image
source={require('../../resources/camera.png')}
style={{
height: 44,
width: 44,
tintColor: Colors.deBtnStandardPrimaryLabel,
}}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={this.flipCamera}
style={{ position: 'absolute', left: 0, bottom: 0 }}
hitSlop={{ left: 10, top: 10, right: 10, bottom: 10 }}>
<Image
source={require('../../resources/reverse-camera.png')}
style={{
height: 32,
width: 32,
tintColor: Colors.deBtnStandardPrimaryLabel,
}}
/>
</TouchableOpacity>
</View>
</SafeAreaView>
</Camera>
);
};

render() {
const { hasCameraPermission } = this.state;
if (hasCameraPermission === null) {
return this.renderNoPermissions();
} else if (hasCameraPermission === false) {
return <Text>No access to camera</Text>;
} else {
const imageUri = `${FileSystem.documentDirectory}${TEMP_PATH}`;
return (
<View style={{ flex: 1 }}>
<SafeAreaView style={sharedStyles.safeArea}>
<Camera
ref={(ref: any) => {
this.camera = ref;
}}
style={{ flex: 1, justifyContent: 'flex-end' }}
type={this.state.type}>
<View
style={{
backgroundColor: 'transparent',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
margin: 16,
}}>
<TouchableOpacity onPress={this.takePicture}>
<Image
source={require('../../resources/camera.png')}
style={{
height: 44,
width: 44,
tintColor: Colors.deBtnStandardPrimaryLabel,
}}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={this.flipCamera}
style={{ position: 'absolute', left: 0, bottom: 0 }}
hitSlop={{ left: 10, top: 10, right: 10, bottom: 10 }}>
<Image
source={require('../../resources/reverse-camera.png')}
style={{
height: 32,
width: 32,
tintColor: Colors.deBtnStandardPrimaryLabel,
}}
/>
</TouchableOpacity>
</View>
</Camera>
</SafeAreaView>
{this.state.showImage ? (
<Preview
imageUri={imageUri}
acceptPhoto={this.acceptPhoto}
declinePhoto={this.declinePhoto}
/>
) : (
this.renderCamera()
)}
</View>
);
}
}
}

const mapDispatchToProps = (dispatch: Dispatch): ActionProps => {
return {
changeImage: bindActionCreators(changeImage, dispatch),
};
};

export default connect(null, mapDispatchToProps)(CameraComponent);
@@ -1,16 +1,23 @@
import React from 'react';
import { View, SafeAreaView, Text } from 'react-native';
import {
View,
SafeAreaView,
Text,
Keyboard,
TouchableWithoutFeedback,
Image,
} from 'react-native';
import { NavigationProps } from '../../navigation/rootNavigation';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import {
saveNewWorkout,
saveExistingWorkout,
saveWorkout,
deleteWorkout,
changeName,
changeType,
changeImage,
} from '../../redux/actions';
import { Dispatch, AppState } from '../../redux';
import { Workout } from '../../types';
import { getWorkout } from '../../redux/selectors';
import { AppRoutes } from '../../navigation/routes';

import Button from '../Button';
@@ -22,22 +29,21 @@ import sharedStyles from '../sharedStyles';
type OwnProps = NavigationProps;

interface StateProps {
workout: Workout | null | undefined;
workoutId: null | string;
name: string;
type: string;
imageUri: string | null;
}

interface ActionProps {
readonly saveNewWorkout: (name: string, type: string, date: Date) => void;
readonly saveExistingWorkout: (
workoutId: string,
name: string,
type: string
) => void;
readonly saveWorkout: (params: { date?: Date; workoutId?: string }) => void;
readonly deleteWorkout: (workoutId: string) => void;
readonly changeName: (name: string) => void;
readonly changeType: (type: string) => void;
readonly changeImage: (imageUri: string) => void;
}

interface LocalState {
name: string;
type: string;
message: string | null;
}

@@ -50,53 +56,40 @@ class EditWorkout extends React.PureComponent<Props, LocalState> {

constructor(props: Props) {
super(props);
this.nameChanged = this.nameChanged.bind(this);

const name = !!this.props.workout
? this.props.workout.name
: `${new Date().toLocaleDateString()} Workout`;
const type = !!this.props.workout ? this.props.workout.type : '';

this.state = {
name,
type,
message: null,
};
}

saveWorkout = () => {
const { workout } = this.props;
const { name, type } = this.state;
const { type, workoutId } = this.props;
if (!type.length) {
this.setState({ message: 'Please enter a workout type.' });
return;
} else {
this.setState({ message: null });
}
if (workout) {
this.props.saveExistingWorkout(workout.id, name, type);
if (workoutId) {
this.props.saveWorkout({ workoutId });
} else {
this.props.saveNewWorkout(name, type, new Date());
this.props.saveWorkout({ date: new Date() });
}
};

deleteWorkout = () => {
const { workout } = this.props;
if (workout) {
this.props.deleteWorkout(workout.id);
const { workoutId } = this.props;
if (workoutId) {
this.props.deleteWorkout(workoutId);
}
};

nameChanged = (text: string) => {
this.setState({
name: text,
});
this.props.changeName(text);
};

typeChanged = (text: string) => {
this.setState({
type: text,
});
this.props.changeType(text);
};

goToCamera = () => {
@@ -106,83 +99,102 @@ class EditWorkout extends React.PureComponent<Props, LocalState> {
};

render() {
const buttonDisabled =
this.state.name.trim().length === 0 ||
this.state.type.trim().length === 0;
const isEditing = !!this.props.workout;
const { name, type, imageUri } = this.props;
const buttonDisabled = name.trim().length === 0 || type.trim().length === 0;
const isEditing = !!this.props.workoutId;
const buttonText = isEditing ? 'Save' : 'Create';
const imageButtonText = !!imageUri ? 'Change Image' : 'Add Image';

return (
<View style={styles.container}>
<SafeAreaView style={sharedStyles.safeArea}>
<View style={styles.content}>
{!!this.state.message ? (
<View style={styles.messageContainer}>
<Text style={styles.message}>{this.state.message}</Text>
</View>
) : null}
<Text style={styles.prompt}>Workout Name</Text>
<Input
value={this.state.name}
onChangeText={this.nameChanged}
style={styles.input}
/>
<Text style={styles.prompt}>Workout Type</Text>
<Input
value={this.state.type}
placeholder="Run, swim, weights, etc."
onChangeText={this.typeChanged}
style={styles.input}
/>
<Button
style={styles.button}
onPress={this.goToCamera}
title="Add Image"
/>
<View style={styles.bottomButtonsContainer}>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<SafeAreaView style={sharedStyles.safeArea}>
<View style={styles.content}>
{!!this.state.message ? (
<View style={styles.messageContainer}>
<Text style={styles.message}>{this.state.message}</Text>
</View>
) : null}
<Text style={styles.prompt}>Workout Name</Text>
<Input
value={name}
onChangeText={this.nameChanged}
style={styles.input}
/>
<Text style={styles.prompt}>Workout Type</Text>
<Input
value={type}
placeholder="Run, swim, weights, etc."
onChangeText={this.typeChanged}
style={styles.input}
/>
{!!imageUri ? (
<Image
source={{ uri: imageUri }}
style={{
width: 90,
height: 160,
resizeMode: 'contain',
marginBottom: 24,
alignSelf: 'center',
}}
/>
) : null}
<Button
style={styles.button}
disabled={buttonDisabled}
onPress={this.saveWorkout}
title={buttonText}
onPress={this.goToCamera}
title={imageButtonText}
/>
{isEditing ? (
<View style={styles.bottomButtonsContainer}>
{isEditing ? (
<Button
style={styles.button}
onPress={this.deleteWorkout}
title="Delete"
destructive={true}
/>
) : null}
<Button
style={styles.button}
onPress={this.deleteWorkout}
title="Delete"
destructive={true}
disabled={buttonDisabled}
onPress={this.saveWorkout}
title={buttonText}
/>
) : null}
</View>
</View>
</View>
</SafeAreaView>
</SafeAreaView>
</TouchableWithoutFeedback>
</View>
);
}
}

const DEFAULT_STATE_PROPS: StateProps = {
workout: null,
workoutId: null,
name: '',
type: '',
imageUri: null,
};
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => {
const workoutId: string | undefined = ownProps.navigation.getParam(
'workoutId'
);
if (!workoutId) {
return DEFAULT_STATE_PROPS;
}

const workout: Workout | undefined = getWorkout(state, workoutId);
const editWorkout = state.editWorkout;
return {
workout,
workoutId: workoutId || DEFAULT_STATE_PROPS.workoutId,
name: editWorkout.name || DEFAULT_STATE_PROPS.name,
type: editWorkout.type || DEFAULT_STATE_PROPS.type,
imageUri: editWorkout.imageUri || DEFAULT_STATE_PROPS.imageUri,
};
};

const mapDispatchToProps = (dispatch: Dispatch): ActionProps => {
return {
saveNewWorkout: bindActionCreators(saveNewWorkout, dispatch),
saveExistingWorkout: bindActionCreators(saveExistingWorkout, dispatch),
saveWorkout: bindActionCreators(saveWorkout, dispatch),
changeName: bindActionCreators(changeName, dispatch),
changeType: bindActionCreators(changeType, dispatch),
changeImage: bindActionCreators(changeImage, dispatch),
deleteWorkout: bindActionCreators(deleteWorkout, dispatch),
};
};
@@ -2,7 +2,7 @@ import React from 'react';
import { View, SafeAreaView, Text } from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { newWorkout } from '../../redux/actions';
import { editWorkout } from '../../redux/actions';
import { Dispatch, AppState } from '../../redux';
import { NavigationProps } from '../../navigation/rootNavigation';
import { values } from '../../util';
@@ -15,7 +15,7 @@ import styles from './style';
import sharedStyles from '../sharedStyles';

interface ActionProps {
readonly newWorkout: () => void;
readonly editWorkout: (workoutId: string | null) => void;
}

interface StateProps {
@@ -30,7 +30,7 @@ class Home extends React.PureComponent<Props> {
};

newWorkout = () => {
this.props.newWorkout();
this.props.editWorkout(null);
};

render() {
@@ -65,7 +65,7 @@ class Home extends React.PureComponent<Props> {

const mapDispatchToProps = (dispatch: Dispatch): ActionProps => {
return {
newWorkout: bindActionCreators(newWorkout, dispatch),
editWorkout: bindActionCreators(editWorkout, dispatch),
};
};

@@ -1,14 +1,21 @@
import { NavigationActions, NavigationAction } from 'react-navigation';
import { AppRoutes } from '../navigation/routes';
import { Dispatch } from './';
import { AppState } from './stateTypes';
import { getWorkout } from './selectors';

export enum ActionKeys {
WorkoutAdd = 'WorkoutAdd',
WorkoutSave = 'WorkoutSave',
WorkoutDelete = 'WorkoutDelete',
EditWorkoutBegin = 'EditWorkoutBegin',
EditWorkoutEnd = 'EditWorkoutEnd',
EditWorkoutChangeName = 'EditWorkoutChangeName',
EditWorkoutChangeType = 'EditWorkoutChangeType',
EditWorkoutChangeImageUri = 'EditWorkoutChangeImageUri',
}

export interface Action<P> {
export interface Action<P = null> {
readonly type: ActionKeys;
readonly payload: P;
}
@@ -18,6 +25,7 @@ export interface WorkoutAdd
readonly name: string;
readonly type: string;
readonly date: Date;
readonly imageUri: string | null;
}> {
type: ActionKeys.WorkoutAdd;
}
@@ -27,6 +35,7 @@ export interface WorkoutSave
readonly workoutId: string;
readonly name: string;
readonly type: string;
readonly imageUri: string | null;
}> {
type: ActionKeys.WorkoutSave;
}
@@ -35,59 +44,145 @@ export interface WorkoutDelete extends Action<string> {
type: ActionKeys.WorkoutDelete;
}

export interface EditWorkoutBegin
extends Action<{
name: string;
type: string | null;
imageUri: string | null;
}> {
type: ActionKeys.EditWorkoutBegin;
}

export interface EditWorkoutEnd extends Action {
type: ActionKeys.EditWorkoutEnd;
}

export interface EditWorkoutChangeName extends Action<string> {
type: ActionKeys.EditWorkoutChangeName;
}

export interface EditWorkoutChangeType extends Action<string> {
type: ActionKeys.EditWorkoutChangeType;
}

export interface EditWorkoutChangeImageUri extends Action<string | null> {
type: ActionKeys.EditWorkoutChangeImageUri;
}

export type ActionTypes =
| WorkoutAdd
| WorkoutDelete
| WorkoutSave
| EditWorkoutBegin
| EditWorkoutEnd
| EditWorkoutChangeName
| EditWorkoutChangeType
| EditWorkoutChangeImageUri
| NavigationAction;

export const newWorkout = () =>
NavigationActions.navigate({
routeName: AppRoutes.EditWorkout,
});
export const editWorkout = (workoutId: string | null) => {
return (dispatch: Dispatch, getState: () => AppState) => {
let payload: {
name: string;
type: string | null;
imageUri: string | null;
} = {
name: `${new Date().toLocaleDateString()} Workout`,
type: null,
imageUri: null,
};
if (!!workoutId) {
const state = getState();
const workout = getWorkout(state, workoutId);
if (!!workout) {
payload = {
name: workout.name,
type: workout.type,
imageUri: workout.imageUri,
};
}
}
const beginEditWorkoutAction: EditWorkoutBegin = {
type: ActionKeys.EditWorkoutBegin,
payload,
};
dispatch(beginEditWorkoutAction);

dispatch(
NavigationActions.navigate({
routeName: AppRoutes.EditWorkout,
params: {
workoutId,
},
})
);
};
};

export const saveWorkout = (params: { date?: Date; workoutId?: string }) => {
return (dispatch: Dispatch, getState: () => AppState) => {
const state = getState();
const { name, type, imageUri } = state.editWorkout;
if (!name || !type) {
return;
}

export const editWorkout = (workoutId: string) =>
NavigationActions.navigate({
routeName: AppRoutes.EditWorkout,
params: {
workoutId,
},
});
if (params.workoutId) {
const saveWorkoutAction: WorkoutSave = {
type: ActionKeys.WorkoutSave,
payload: {
workoutId: params.workoutId,
name,
type,
imageUri,
},
};
dispatch(saveWorkoutAction);
} else if (params.date) {
if (!!name && !!type) {
const addWorkoutAction: WorkoutAdd = {
type: ActionKeys.WorkoutAdd,
payload: {
name,
date: params.date,
imageUri,
type,
},
};
dispatch(addWorkoutAction);
}
}

export const saveNewWorkout = (name: string, type: string, date: Date) => {
return (dispatch: Dispatch) => {
const addWorkoutAction: WorkoutAdd = {
type: ActionKeys.WorkoutAdd,
payload: {
name,
date,
type,
},
const endEditWorkoutAction: EditWorkoutEnd = {
type: ActionKeys.EditWorkoutEnd,
payload: null,
};
dispatch(addWorkoutAction);
dispatch(endEditWorkoutAction);
dispatch(NavigationActions.back({}));
};
};

export const saveExistingWorkout = (
workoutId: string,
name: string,
type: string
) => {
export const changeImage = (uri: string) => {
return (dispatch: Dispatch) => {
const saveWorkoutAction: WorkoutSave = {
type: ActionKeys.WorkoutSave,
payload: {
workoutId,
name,
type,
},
const newWorkoutChangeImageUriAction: EditWorkoutChangeImageUri = {
type: ActionKeys.EditWorkoutChangeImageUri,
payload: uri,
};
dispatch(saveWorkoutAction);
dispatch(newWorkoutChangeImageUriAction);
dispatch(NavigationActions.back({}));
};
};

export const changeName = (name: string): EditWorkoutChangeName => ({
type: ActionKeys.EditWorkoutChangeName,
payload: name,
});

export const changeType = (type: string): EditWorkoutChangeType => ({
type: ActionKeys.EditWorkoutChangeType,
payload: type,
});

export const deleteWorkout = (id: string) => {
return (dispatch: Dispatch) => {
const deleteWorkoutAction: WorkoutDelete = {
@@ -0,0 +1,34 @@
import { ActionKeys, ActionTypes } from './actions';
import { EditWorkout } from './stateTypes';

const initialState = {
name: null,
type: null,
imageUri: null,
};

const editWorkoutReducer = (
state: EditWorkout = initialState,
action: ActionTypes
): EditWorkout => {
switch (action.type) {
case ActionKeys.EditWorkoutBegin:
const { name, type, imageUri } = action.payload;
return {
name,
type,
imageUri,
};
case ActionKeys.EditWorkoutEnd:
return initialState;
case ActionKeys.EditWorkoutChangeName:
return { ...state, name: action.payload };
case ActionKeys.EditWorkoutChangeType:
return { ...state, type: action.payload };
case ActionKeys.EditWorkoutChangeImageUri:
return { ...state, imageUri: action.payload };
default:
return state;
}
};
export default editWorkoutReducer;
@@ -1,17 +1,16 @@
import { combineReducers } from 'redux';
import { Dispatch as ReduxDispatch } from 'redux';
import { NavigationState } from 'react-navigation';
import nav from './navReducer';
import workouts, { Workouts } from './workoutReducer';
import workouts from './workoutReducer';
import editWorkout from './editWorkoutReducer';
import { AppState } from './stateTypes';

export interface AppState {
nav: NavigationState;
workouts: Workouts;
}
export * from './stateTypes';

export type Dispatch = ReduxDispatch<AppState>;

export default combineReducers<AppState>({
nav,
workouts,
editWorkout,
});
@@ -0,0 +1,18 @@
import { NavigationState } from 'react-navigation';
import { Workout } from '../types';

export interface Workouts {
[workoutId: string]: Workout;
}

export interface EditWorkout {
name: string | null;
type: string | null;
imageUri: string | null;
}

export interface AppState {
nav: NavigationState;
workouts: Workouts;
editWorkout: EditWorkout;
}
@@ -1,9 +1,6 @@
import { ActionKeys, ActionTypes } from './actions';
import { Workout } from '../types';

export interface Workouts {
[workoutId: string]: Workout;
}
import { Workouts } from './stateTypes';

const initialState = {};

@@ -17,7 +14,8 @@ export default function workoutsReducer(
uuidv4(),
action.payload.name,
action.payload.type,
action.payload.date
action.payload.date,
action.payload.imageUri
);
return {
...state,
@@ -34,7 +32,8 @@ export default function workoutsReducer(
existingWorkout.id,
action.payload.name,
action.payload.type,
existingWorkout.date
existingWorkout.date,
action.payload.imageUri
);
return {
...state,
@@ -4,6 +4,7 @@ export class Workout {
public id: string,
public name: string,
public type: string,
public date: Date
public date: Date,
public imageUri: string | null
) {}
}