Merge branch 'master' into loewy/session-details

This commit is contained in:
Loewy 2024-02-21 17:58:56 -08:00
commit 608e784205
17 changed files with 481 additions and 255 deletions

View File

@ -155,7 +155,7 @@ export default function CameraScreen({
const onMediaCaptured = useCallback(
(media: PhotoFile | VideoFile) => {
console.log(`Media captured! ${JSON.stringify(media)}`);
navigation.push("SaveVideo", {
navigation.push("SaveRecording", {
videoId: uploadManager?.videoId,
...params,
});

View File

@ -54,7 +54,7 @@ function useDropdown<T>(
};
}
export const useVideoDetails = ({
export const useRecordingDetails = ({
params: {
mode,
videoId,
@ -118,7 +118,7 @@ export const useVideoDetails = ({
}
// Navigate if starting flow, terminateUploadStream if completing flow
if (mode === "start-video") {
if (mode === "start-recording") {
const params: VideoFlowInputParams = {
sessionName,
gameType: gameType.value,

View File

@ -0,0 +1,26 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
const VideoCardFooter = ({ videoName, lastPlayed }) => {
return (
<View>
<Text style={styles.videoName}>{videoName}</Text>
<Text style={styles.videoDatetime}>{lastPlayed}</Text>
</View>
);
};
const styles = StyleSheet.create({
videoName: {
fontSize: 18,
paddingTop: 5,
marginHorizontal: 16,
},
videoDatetime: {
fontSize: 10,
color: "#A3A3A3",
marginHorizontal: 16,
},
});
export default VideoCardFooter;

View File

@ -0,0 +1,68 @@
import React from "react";
import { Image, StyleSheet, Text, View } from "react-native";
const VideoCardHeader = ({
playerName,
location,
gameType,
locationIconURL,
profileImageURL,
}) => {
return (
<View style={styles.headerContainer}>
<Image
style={styles.headerProfileImage}
source={{ uri: profileImageURL }}
accessibilityLabel="Profile image"
/>
<View style={styles.headerText}>
<Text style={styles.playerName}>{playerName}</Text>
<View style={styles.locationContainer}>
<Image source={{ uri: locationIconURL }} style={styles.icon} />
<Text style={styles.locationText}>{location}</Text>
</View>
<Text style={styles.gameType}>{gameType}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
headerContainer: {
flexDirection: "row",
alignItems: "center",
marginHorizontal: 16,
},
headerProfileImage: {
width: 60,
height: 60,
resizeMode: "contain",
},
headerText: {
flexDirection: "column",
padding: 10,
},
playerName: {
fontSize: 24,
fontWeight: "bold",
},
locationContainer: {
flexDirection: "row",
alignItems: "center",
},
locationText: {
fontSize: 13,
},
gameType: {
fontSize: 13,
},
icon: {
width: 18,
height: 18,
resizeMode: "contain",
},
});
export default VideoCardHeader;

View File

@ -0,0 +1,25 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
const VideoCardStat = ({ videoStat, displayName }) => {
return (
<View>
<Text style={styles.statItem}>{displayName}</Text>
<Text style={styles.statValue}>{videoStat}</Text>
</View>
);
};
const styles = StyleSheet.create({
statItem: {
fontSize: 10,
textAlign: "center",
color: "#666",
},
statValue: {
fontSize: 22,
fontWeight: "400",
},
});
export default VideoCardStat;

View File

@ -0,0 +1,48 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import VideoCardStat from "./video-card-stat";
const VideoCardStatsRowContainer = ({
makePercent,
medianRun,
duration,
shotPacing,
}) => {
const stats = [
{ displayName: "Make Percent", videoStat: makePercent },
{ displayName: "Median Run", videoStat: medianRun },
{ displayName: "Time Played", videoStat: duration },
{ displayName: "Shot Pacing", videoStat: shotPacing },
];
return (
<View style={styles.statsContainer}>
{stats.map((stat, index) => (
<React.Fragment key={index}>
<VideoCardStat
displayName={stat.displayName}
videoStat={stat.videoStat}
/>
{index < stats.length - 1 && <View style={styles.verticalSpacer} />}
</React.Fragment>
))}
</View>
);
};
const styles = StyleSheet.create({
statsContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
margin: 16,
},
verticalSpacer: {
width: 1,
backgroundColor: "#A3A3A3",
height: "100%",
marginHorizontal: 12,
},
});
export default VideoCardStatsRowContainer;

View File

@ -0,0 +1,65 @@
import React from "react";
import { Image, StyleSheet, View } from "react-native";
import VideoCardFooter from "./video-card-footer";
import VideoCardHeader from "./video-card-header";
import VideoCardStatsRowContainer from "./video-card-stats-row-container";
const VideoCard = ({
playerName,
location,
gameType,
makePercent,
medianRun,
duration,
shotPacing,
videoName,
lastPlayed,
imageURL,
profileImageURL,
locationIconURL,
}) => {
return (
<View style={styles.card}>
<VideoCardHeader
playerName={playerName}
location={location}
gameType={gameType}
locationIconURL={locationIconURL}
profileImageURL={profileImageURL}
/>
<VideoCardStatsRowContainer
makePercent={makePercent}
medianRun={medianRun}
duration={duration}
shotPacing={shotPacing}
/>
<Image source={imageURL} style={styles.image} />
<VideoCardFooter videoName={videoName} lastPlayed={lastPlayed} />
</View>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: "white",
borderRadius: 8,
borderWidth: 1,
borderColor: "#ddd",
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
margin: 10,
overflow: "hidden",
},
image: {
width: "100%",
height: "50%",
},
});
export default VideoCard;

View File

@ -1,11 +1,11 @@
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Image } from "react-native";
import CameraScreen from "../component/video/camera";
import CameraScreen from "../component/recording/camera";
import Profile from "../screens/profile";
import Session from "../screens/session-stack/session";
import SessionFeed from "../screens/session-stack/session-feed";
import VideoDetails from "../screens/video-stack/video-details";
import RecordingDetails from "../screens/recording-stack/recording-details";
import Video from "../screens/video-stack/video-details";
import VideoFeed from "../screens/video-stack/video-feed";
import { tabIconColors } from "../styles";
import Icon from "../assets/icons/favicon.png";
@ -15,34 +15,34 @@ const RecordStack = createNativeStackNavigator();
// tabBarIcon configuration should live on separate file and contain all logic/icons/rendering for the Tabs
const tabIcons = {
SessionStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
VideoStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
RecordingStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
Profile: <Image source={Icon} style={{ width: 20, height: 20 }} />,
};
function VideoTabStack() {
function RecordingTabStack() {
return (
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
<RecordStack.Screen
name="Record"
component={VideoDetails}
initialParams={{ mode: "start-video" }}
component={RecordingDetails}
initialParams={{ mode: "start-recording" }}
/>
<RecordStack.Screen name="Camera" component={CameraScreen} />
<RecordStack.Screen
name="SaveVideo"
component={VideoDetails}
initialParams={{ mode: "save-video" }}
name="SaveRecording"
component={RecordingDetails}
initialParams={{ mode: "save-recording" }}
/>
</RecordStack.Navigator>
);
}
function SessionTabStack() {
function VideoTabStack() {
return (
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
<RecordStack.Screen name="SessionFeed" component={SessionFeed} />
<RecordStack.Screen name="Session" component={Session} />
<RecordStack.Screen name="VideoFeed" component={VideoFeed} />
<RecordStack.Screen name="Video" component={Video} />
</RecordStack.Navigator>
);
}
@ -67,14 +67,14 @@ export default function Tabs(): React.JSX.Element {
},
})}
>
<Tab.Screen
name="SessionStack"
component={SessionTabStack}
options={{ tabBarLabel: "Session" }}
/>
<Tab.Screen
name="VideoStack"
component={VideoTabStack}
options={{ tabBarLabel: "Feed" }}
/>
<Tab.Screen
name="RecordingStack"
component={RecordingTabStack}
options={{ tabBarLabel: "Record" }}
/>
<Tab.Screen

View File

@ -0,0 +1,94 @@
import React from "react";
import {
ActivityIndicator,
Keyboard,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from "react-native";
import DropDownPicker from "react-native-dropdown-picker";
import { useRecordingDetails } from "../../component/recording/use-recording-details";
import { globalInputStyles } from "../../styles";
import { recordStyles as styles } from "./styles";
export default function VideoDetails({ navigation, route }): React.JSX.Element {
const { mode } = route.params;
const {
sessionName,
setSessionName,
gameType,
tableSize,
handleSubmit,
loading,
} = useRecordingDetails({ params: route.params, navigation });
const dropDownStyles = {
style: globalInputStyles.dropdownStyle,
};
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<Text style={styles.headerText}>
{mode === "start-recording" ? "Record Session" : "Save Session"}
</Text>
<View style={globalInputStyles.dropdownContainer}>
<Text style={globalInputStyles.dropdownTitle}>Session Name</Text>
<TextInput
style={globalInputStyles.input}
placeholder="Session name"
autoCapitalize="none"
value={sessionName}
onChangeText={setSessionName}
/>
<Text style={globalInputStyles.dropdownTitle}>Game Type</Text>
<DropDownPicker
zIndex={3000}
zIndexInverse={1000}
open={gameType.isDropdownOpen}
value={gameType.value}
items={gameType.optionsList}
setOpen={gameType.toggleOpen}
setValue={gameType.setValue}
setItems={gameType.setOptionsList}
{...dropDownStyles}
/>
<Text style={globalInputStyles.dropdownTitle}>Table Size</Text>
<DropDownPicker
zIndex={2000}
zIndexInverse={2000}
open={tableSize.isDropdownOpen}
value={tableSize.value}
items={tableSize.optionsList}
setOpen={tableSize.toggleOpen}
setValue={tableSize.setValue}
setItems={tableSize.setOptionsList}
{...dropDownStyles}
/>
</View>
<View style={styles.buttonContainer}>
{mode === "start-recording" && (
<TouchableOpacity
style={styles.buttonStyle}
onPress={() => navigation.goBack()}
>
<Text style={styles.buttonText}>Back</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>
{mode === "start-recording" ? "Next" : "Save"}
</Text>
)}
</TouchableOpacity>
</View>
</View>
</TouchableWithoutFeedback>
);
}

View File

@ -1,6 +0,0 @@
import React from "react";
import VideoDetails from "./video-details";
export default function SessionScreen({ navigation }) {
return <VideoDetails navigation={navigation} />;
}

View File

@ -1,129 +0,0 @@
import React from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import {
graph_data_two_measures,
mock_session_details,
} from "../../../test/mock/charts/mock-data";
import BarGraph from "../../component/charts/bar-graph/bar-graph";
import ChartContainer from "../../component/charts/container/chart-container";
import ImageWithFallback from "../../component/image/image-with-fallback";
import StatList from "../../component/video-details/video-stat-list";
// TODO: #134 remove Session when data piped through
// Splash should be an asset we use if an Image failed to load
import Session from "../../assets/sample_session.png";
import Splash from "../../assets/splash.png";
import BackHeader from "../../component/headers/back-header";
import { borders, colors } from "../../styles";
export default function VideoDetails({ navigation }) {
// TODO: #134 Remove mock destructure block when data is piped through from BE
const {
sessionTitle,
date,
timePlayed,
medianRun,
makeRate,
shotPacing,
gameType,
notes,
} = mock_session_details;
const leftColumnStats = [
{ title: "TIME PLAYED", value: timePlayed },
{ title: "MAKE RATE", value: makeRate },
];
const rightColumnStats = [
{ title: "MEDIAN RUN", value: medianRun },
{ title: "SHOT PACING", value: shotPacing },
];
// End mock destructure
return (
<>
<BackHeader navigation={navigation} title="Go Back" />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContainer}
>
<View style={styles.headerSection}>
<Text style={styles.header}>{sessionTitle}</Text>
<Text>{date}</Text>
</View>
<ImageWithFallback
// TODO: #134 when data comes from be needs to be passed as source={{ uri: image }}
source={Session}
fallbackSource={Splash}
style={styles.image}
/>
<View style={styles.statsContainer}>
<StatList items={leftColumnStats} style={styles.statColumn} />
<StatList items={rightColumnStats} style={styles.statColumn} />
</View>
<View style={styles.horizontalDivider} />
<View style={styles.textContainer}>
<Text style={styles.title}>Game Type</Text>
<Text style={styles.text}>{gameType}</Text>
<Text style={styles.title}>Notes</Text>
<Text style={styles.text}>{notes}</Text>
</View>
<View style={[styles.horizontalDivider, { marginVertical: 21 }]} />
<ChartContainer
data={graph_data_two_measures}
ChartComponent={BarGraph}
/>
</ScrollView>
</>
);
}
// TODO: #130 scaled styles + maintain consistency with video-feed styles
const styles = StyleSheet.create({
scrollContainer: {
backgroundColor: "white", // TODO #125 -- this color should not be set but implicitly inherit from theme
paddingBottom: 20, // guarantees some space at bottom of scrollable views
},
headerSection: {
paddingHorizontal: 38,
paddingTop: 17,
paddingBottom: 14,
},
header: {
fontSize: 24,
fontWeight: "bold",
},
image: {
width: "100%",
height: 248,
},
statsContainer: {
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: 45,
paddingVertical: 42,
},
statColumn: {
flex: 1,
padding: 5,
},
horizontalDivider: {
width: "95%",
alignSelf: "center",
...borders.dottedBottomBorder,
},
textContainer: {
paddingHorizontal: 38,
},
title: {
fontSize: 16,
color: colors.text.greyText,
paddingTop: 16,
},
text: {
fontSize: 18,
textAlign: "left",
paddingTop: 6,
fontWeight: "400",
},
});

View File

@ -1,94 +1,129 @@
import React from "react";
import { ScrollView, StyleSheet, Text, View } from "react-native";
import {
ActivityIndicator,
Keyboard,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from "react-native";
import DropDownPicker from "react-native-dropdown-picker";
import { useVideoDetails } from "../../component/video/use-video-details";
import { globalInputStyles } from "../../styles";
import { recordStyles as styles } from "./styles";
graph_data_two_measures,
mock_session_details,
} from "../../../test/mock/charts/mock-data";
import BarGraph from "../../component/charts/bar-graph/bar-graph";
import ChartContainer from "../../component/charts/container/chart-container";
import ImageWithFallback from "../../component/image/image-with-fallback";
import StatList from "../../component/video-details/video-stat-list";
export default function VideoDetails({ navigation, route }): React.JSX.Element {
const { mode } = route.params;
// TODO: #134 remove Session when data piped through
// Splash should be an asset we use if an Image failed to load
import Session from "../../assets/sample_session.png";
import Splash from "../../assets/splash.png";
import BackHeader from "../../component/headers/back-header";
import { borders, colors } from "../../styles";
export default function VideoDetails({ navigation }) {
// TODO: #134 Remove mock destructure block when data is piped through from BE
const {
sessionName,
setSessionName,
sessionTitle,
date,
timePlayed,
medianRun,
makeRate,
shotPacing,
gameType,
tableSize,
handleSubmit,
loading,
} = useVideoDetails({ params: route.params, navigation });
notes,
} = mock_session_details;
const dropDownStyles = {
style: globalInputStyles.dropdownStyle,
};
const leftColumnStats = [
{ title: "TIME PLAYED", value: timePlayed },
{ title: "MAKE RATE", value: makeRate },
];
const rightColumnStats = [
{ title: "MEDIAN RUN", value: medianRun },
{ title: "SHOT PACING", value: shotPacing },
];
// End mock destructure
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<View style={styles.container}>
<Text style={styles.headerText}>
{mode === "start-video" ? "Record Session" : "Save Session"}
</Text>
<View style={globalInputStyles.dropdownContainer}>
<Text style={globalInputStyles.dropdownTitle}>Session Name</Text>
<TextInput
style={globalInputStyles.input}
placeholder="Session name"
autoCapitalize="none"
value={sessionName}
onChangeText={setSessionName}
/>
<Text style={globalInputStyles.dropdownTitle}>Game Type</Text>
<DropDownPicker
zIndex={3000}
zIndexInverse={1000}
open={gameType.isDropdownOpen}
value={gameType.value}
items={gameType.optionsList}
setOpen={gameType.toggleOpen}
setValue={gameType.setValue}
setItems={gameType.setOptionsList}
{...dropDownStyles}
/>
<Text style={globalInputStyles.dropdownTitle}>Table Size</Text>
<DropDownPicker
zIndex={2000}
zIndexInverse={2000}
open={tableSize.isDropdownOpen}
value={tableSize.value}
items={tableSize.optionsList}
setOpen={tableSize.toggleOpen}
setValue={tableSize.setValue}
setItems={tableSize.setOptionsList}
{...dropDownStyles}
/>
<>
<BackHeader navigation={navigation} title="Go Back" />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContainer}
>
<View style={styles.headerSection}>
<Text style={styles.header}>{sessionTitle}</Text>
<Text>{date}</Text>
</View>
<View style={styles.buttonContainer}>
{mode === "start-video" && (
<TouchableOpacity
style={styles.buttonStyle}
onPress={() => navigation.goBack()}
>
<Text style={styles.buttonText}>Back</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>
{mode === "start-video" ? "Next" : "Save"}
</Text>
)}
</TouchableOpacity>
<ImageWithFallback
// TODO: #134 when data comes from be needs to be passed as source={{ uri: image }}
source={Session}
fallbackSource={Splash}
style={styles.image}
/>
<View style={styles.statsContainer}>
<StatList items={leftColumnStats} style={styles.statColumn} />
<StatList items={rightColumnStats} style={styles.statColumn} />
</View>
</View>
</TouchableWithoutFeedback>
<View style={styles.horizontalDivider} />
<View style={styles.textContainer}>
<Text style={styles.title}>Game Type</Text>
<Text style={styles.text}>{gameType}</Text>
<Text style={styles.title}>Notes</Text>
<Text style={styles.text}>{notes}</Text>
</View>
<View style={[styles.horizontalDivider, { marginVertical: 21 }]} />
<ChartContainer
data={graph_data_two_measures}
ChartComponent={BarGraph}
/>
</ScrollView>
</>
);
}
// TODO: #130 scaled styles + maintain consistency with video-feed styles
const styles = StyleSheet.create({
scrollContainer: {
backgroundColor: "white", // TODO #125 -- this color should not be set but implicitly inherit from theme
paddingBottom: 20, // guarantees some space at bottom of scrollable views
},
headerSection: {
paddingHorizontal: 38,
paddingTop: 17,
paddingBottom: 14,
},
header: {
fontSize: 24,
fontWeight: "bold",
},
image: {
width: "100%",
height: 248,
},
statsContainer: {
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: 45,
paddingVertical: 42,
},
statColumn: {
flex: 1,
padding: 5,
},
horizontalDivider: {
width: "95%",
alignSelf: "center",
...borders.dottedBottomBorder,
},
textContainer: {
paddingHorizontal: 38,
},
title: {
fontSize: 16,
color: colors.text.greyText,
paddingTop: 16,
},
text: {
fontSize: 18,
textAlign: "left",
paddingTop: 6,
fontWeight: "400",
},
});

View File

@ -1,28 +1,28 @@
import { StackNavigationProp } from "@react-navigation/stack";
import React from "react";
import { StyleSheet, TouchableOpacity, View } from "react-native";
import sampleSessionImage from "../../assets/sample_session.png";
import SessionCard from "../../component/session-card/session-card";
import sampleVideoImage from "../../assets/sample_session.png";
import VideoCard from "../../component/video-card/video-card";
// Define the types for your navigation stack
type SessionStackParamList = {
Session: undefined; // Add other screens as needed
type VideoStackParamList = {
Video: undefined; // Add other screens as needed
};
type SessionFeedNavigationProp = StackNavigationProp<
SessionStackParamList,
"Session"
type VideoFeedNavigationProp = StackNavigationProp<
VideoStackParamList,
"Video"
>;
// Define the props for SessionFeed component
interface SessionFeedProps {
navigation: SessionFeedNavigationProp;
// Define the props for VideoFeed component
interface VideoFeedProps {
navigation: VideoFeedNavigationProp;
}
const SessionFeed: React.FC<SessionFeedProps> = ({ navigation }) => {
const VideoFeed: React.FC<VideoFeedProps> = ({ navigation }) => {
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => navigation.push("Session")}>
<SessionCard
<TouchableOpacity onPress={() => navigation.push("Video")}>
<VideoCard
playerName="Dean Machine"
location="Family Billiards, San Francisco"
gameType="Straight Pool"
@ -30,8 +30,8 @@ const SessionFeed: React.FC<SessionFeedProps> = ({ navigation }) => {
medianRun="7.3"
duration="5:03:10"
shotPacing="0:00:26"
imageURL={sampleSessionImage}
sessionName="Dusting off the chalk"
imageURL={sampleVideoImage}
videoName="Dusting off the chalk"
lastPlayed="Today at 2:37pm"
profileImageURL="https://www.pngall.com/wp-content/uploads/5/Profile-PNG-File.png"
locationIconURL="https://www.shutterstock.com/image-vector/blank-map-marker-vector-illustration-260nw-1150566347.jpg"
@ -49,4 +49,4 @@ const styles = StyleSheet.create({
},
});
export default SessionFeed;
export default VideoFeed;