Merge pull request 'Video Details -- UI w/ mocked data' (#136) from loewy/session-details into master

Reviewed-on: railbird/railbird-mobile#136
This commit is contained in:
Kat Huang 2024-02-22 18:11:07 -07:00
commit a47fb5fed7
11 changed files with 372 additions and 33 deletions

View File

@ -19,7 +19,7 @@
"jest": { "jest": {
"preset": "jest-expo", "preset": "jest-expo",
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-native-svg-charts|d3-path)/)" "node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|react-native-svg-charts|d3-path|expo-constants|expo-modules-core)/)"
] ]
}, },
"dependencies": { "dependencies": {
@ -57,6 +57,7 @@
"react-native-dotenv": "^3.4.9", "react-native-dotenv": "^3.4.9",
"react-native-dropdown-picker": "^5.4.6", "react-native-dropdown-picker": "^5.4.6",
"react-native-fs": "^2.20.0", "react-native-fs": "^2.20.0",
"react-native-modal": "^13.0.1",
"react-native-reanimated": "^3.6.2", "react-native-reanimated": "^3.6.2",
"react-native-safe-area-context": "^4.8.2", "react-native-safe-area-context": "^4.8.2",
"react-native-screens": "~3.22.0", "react-native-screens": "~3.22.0",

View File

@ -0,0 +1,42 @@
import { NavigationProp } from "@react-navigation/native";
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { shadows } from "../../styles";
type BackHeaderProps = {
navigation: NavigationProp<any>; // TODO: #135 should use a RootStackParamList
title: string;
};
const BackHeader: React.FC<BackHeaderProps> = ({ navigation, title }) => {
return (
<View style={styles.container}>
<TouchableOpacity
style={styles.button}
onPress={() => navigation.goBack()}
accessibilityLabel="Go back"
accessibilityRole="button"
>
<Text style={styles.text}>{`< ${title}`}</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
zIndex: 100,
...shadows.standard,
},
button: {
padding: 10,
},
text: {
fontSize: 16,
fontWeight: "500",
color: "black",
},
});
export default BackHeader;

View File

@ -0,0 +1,34 @@
import React, { memo, useState } from "react";
import { Image, ImageStyle, StyleProp } from "react-native";
type ImageWithFallbackProps = {
source: { uri: string };
fallbackSource: { uri: string };
style?: StyleProp<ImageStyle>;
};
/**
* A React component that displays an image with a fallback option.
* If the primary image fails to load, it will display a fallback image instead.
*
* @param {Object} source - The source of the primary image.
* @param {Object} fallbackSource - The source of the fallback image. Should be an asset rather than loaded from an endpoint.
* @param {StyleProp<ImageStyle>} style - Optional. The style to apply to the image. It can be any valid React Native style object.
* @returns {React.ReactElement} A React element representing the image with fallback.
*/
const ImageWithFallback: React.FC<ImageWithFallbackProps> = memo(
({ source, fallbackSource, style }) => {
const [imageSource, setImageSource] = useState(source);
const handleError = () => {
if (fallbackSource) {
setImageSource(fallbackSource);
}
};
return <Image source={imageSource} style={style} onError={handleError} />;
},
);
ImageWithFallback.displayName = "ImageWithFallback";
export default ImageWithFallback;

View File

@ -0,0 +1,34 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
// Type assumes preformatted date
type SessionHeaderProps = {
sessionTitle: string;
date: string;
};
const SessionHeader: React.FC<SessionHeaderProps> = ({
sessionTitle,
date,
}) => {
return (
<View style={styles.headerSection}>
<Text style={styles.header}>{sessionTitle}</Text>
<Text>{date}</Text>
</View>
);
};
const styles = StyleSheet.create({
headerSection: {
paddingHorizontal: 38,
paddingTop: 17,
paddingBottom: 14,
},
header: {
fontSize: 24,
fontWeight: "bold",
},
});
export default SessionHeader;

View File

@ -0,0 +1,49 @@
import React from "react";
import { StyleProp, StyleSheet, Text, View, ViewStyle } from "react-native";
import { borders } from "../../styles";
type StatItem = {
title: string;
value: string;
};
type StatListProps = {
items: StatItem[];
style?: StyleProp<ViewStyle>;
};
const StatList: React.FC<StatListProps> = ({ items, style }) => {
return (
<View style={[styles.container, style]}>
{items.map((item, index) => (
<View key={`${item.title}-${index}`} style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.value}>{item.value}</Text>
</View>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 5,
},
item: {
marginBottom: 10,
...borders.dottedLeftBorder,
},
title: {
fontSize: 15,
color: "grey",
textAlign: "center",
},
value: {
fontSize: 28,
fontWeight: "bold",
textAlign: "center",
},
});
export default StatList;

View File

@ -4,7 +4,7 @@ import { Image } from "react-native";
import CameraScreen from "../component/recording/camera"; import CameraScreen from "../component/recording/camera";
import Profile from "../screens/profile"; import Profile from "../screens/profile";
import RecordingDetails from "../screens/recording-stack/recording-details"; import RecordingDetails from "../screens/recording-stack/recording-details";
import Video from "../screens/video-stack/video-detail"; import Video from "../screens/video-stack/video-details";
import VideoFeed from "../screens/video-stack/video-feed"; import VideoFeed from "../screens/video-stack/video-feed";
import { tabIconColors } from "../styles"; import { tabIconColors } from "../styles";
@ -70,7 +70,7 @@ export default function Tabs(): React.JSX.Element {
<Tab.Screen <Tab.Screen
name="VideoStack" name="VideoStack"
component={VideoTabStack} component={VideoTabStack}
options={{ tabBarLabel: "Video" }} options={{ tabBarLabel: "Feed" }}
/> />
<Tab.Screen <Tab.Screen
name="RecordingStack" name="RecordingStack"

View File

@ -1,21 +0,0 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import {
graph_data_two_measures,
line_chart_two_y_data,
} from "../../../test/mock/charts/mock-data";
import BarGraph from "../../component/charts/bar-graph/bar-graph";
import ChartContainer from "../../component/charts/container/chart-container";
import LineGraph from "../../component/charts/line-graph/line-graph";
export default function VideoScreen() {
return (
<View style={StyleSheet.absoluteFill}>
<ChartContainer data={line_chart_two_y_data} ChartComponent={LineGraph} />
<ChartContainer
data={graph_data_two_measures}
ChartComponent={BarGraph}
/>
</View>
);
}

View File

@ -0,0 +1,119 @@
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 SessionHeader from "../../component/video-details/video-details-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}
>
<SessionHeader sessionTitle={sessionTitle} date={date} />
<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
},
image: {
width: "100%",
height: 248,
},
statsContainer: {
flexDirection: "row",
justifyContent: "space-between",
paddingLeft: 45,
paddingTop: 42,
paddingBottom: 38,
},
statColumn: {
flex: 1,
padding: 5,
},
horizontalDivider: {
width: "92%",
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,6 +1,14 @@
// GLOBAL STYLES import Constants from "expo-constants";
import { StyleSheet } from "react-native"; import { Platform, StyleSheet } from "react-native";
// STYLE CONSTS
let STATUS_BAR_HEIGHT: number;
Platform.OS === "android"
? (STATUS_BAR_HEIGHT = 24)
: (STATUS_BAR_HEIGHT = Constants.statusBarHeight || 24);
// GLOBALLY STYLED
export const globalInputStyles = StyleSheet.create({ export const globalInputStyles = StyleSheet.create({
input: { input: {
width: "100%", width: "100%",
@ -28,18 +36,21 @@ export const globalInputStyles = StyleSheet.create({
borderRadius: 4, borderRadius: 4,
}, },
}); });
// COLORS: // COLORS:
// can be made more granular to specify utility (ex: fontColors vs backgroundColors) // can be made more granular to specify utility (ex: fontColors vs backgroundColors)
export const colors = { export const colors = {
bgBlack: "#121212", bgBlack: "#121212",
lightGrey: "#BFC2C8", lightGrey: "#BFC2C8",
darkGrey: "#767577", darkGrey: "#767577",
themeBrown: "#D9AA84",
panelWhite: "#F2FBFE", panelWhite: "#F2FBFE",
tournamentBlue: "#50a6c2",
blueCloth: "#539dc2",
buttonBlue: "#1987ff", buttonBlue: "#1987ff",
textWhite: "#ffffff", border: {
grey: "#8F8E94",
},
text: {
greyText: "#848580",
},
}; };
export const tabIconColors = { export const tabIconColors = {
@ -47,7 +58,8 @@ export const tabIconColors = {
selected: "#598EBB", selected: "#598EBB",
}; };
export const shadows = { // SHADOWS
export const shadows = StyleSheet.create({
standard: { standard: {
shadowColor: "#000000", shadowColor: "#000000",
shadowOffset: { shadowOffset: {
@ -58,4 +70,47 @@ export const shadows = {
shadowRadius: 4.65, shadowRadius: 4.65,
elevation: 3, elevation: 3,
}, },
}; });
// BORDER
export const borders = StyleSheet.create({
dottedBottomBorder: {
borderBottomWidth: 1,
borderStyle: "dotted",
color: colors.border.grey,
},
dottedLeftBorder: {
borderLeftWidth: 1,
borderStyle: "dotted",
color: colors.border.grey,
},
});
// MODAL
const MODAL_TOP_PADDING = 20;
const MODAL_TOP_RADIUS = 20;
export const modal = StyleSheet.create({
alignModalContainer: {
flex: 1,
marginTop: STATUS_BAR_HEIGHT + MODAL_TOP_PADDING ?? 54 + MODAL_TOP_PADDING,
backgroundColor: "white",
borderTopLeftRadius: MODAL_TOP_RADIUS,
borderTopRightRadius: MODAL_TOP_RADIUS,
width: "100%",
},
contentStyles: {
paddingBottom: 20,
},
grabber: {
alignSelf: "center",
width: 40,
height: 5,
backgroundColor: "#ccc",
borderRadius: 2.5,
marginVertical: 15,
},
noMargin: {
margin: 0,
},
});

View File

@ -97,3 +97,14 @@ export const mockYData = [
]; ];
export const mockXValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; export const mockXValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export const mock_session_details = {
sessionTitle: "Afternoon Session",
date: "Today at 2:37pm",
timePlayed: "5:03:10",
medianRun: "7.3",
makeRate: "34%",
shotPacing: "0:00:26",
gameType: "Free Play",
notes: "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
};

View File

@ -9889,7 +9889,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0, prompts@^2.4.2:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@*, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@*, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -10041,6 +10041,13 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-native-animatable@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.3.3.tgz#a13a4af8258e3bb14d0a9d839917e9bb9274ec8a"
integrity sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==
dependencies:
prop-types "^15.7.2"
react-native-dotenv@^3.4.9: react-native-dotenv@^3.4.9:
version "3.4.9" version "3.4.9"
resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.9.tgz#621c5b0c1d0c5c7f569bfe5a1d804bec7885c010" resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.9.tgz#621c5b0c1d0c5c7f569bfe5a1d804bec7885c010"
@ -10061,6 +10068,14 @@ react-native-fs@^2.20.0:
base-64 "^0.1.0" base-64 "^0.1.0"
utf8 "^3.0.0" utf8 "^3.0.0"
react-native-modal@^13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/react-native-modal/-/react-native-modal-13.0.1.tgz#691f1e646abb96fa82c1788bf18a16d585da37cd"
integrity sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==
dependencies:
prop-types "^15.6.2"
react-native-animatable "1.3.3"
react-native-reanimated@^3.6.2: react-native-reanimated@^3.6.2:
version "3.6.2" version "3.6.2"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.2.tgz#8a48c37251cbd3b665a659444fa9778f5b510356" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.2.tgz#8a48c37251cbd3b665a659444fa9778f5b510356"