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:
commit
a47fb5fed7
@ -19,7 +19,7 @@
|
||||
"jest": {
|
||||
"preset": "jest-expo",
|
||||
"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": {
|
||||
@ -57,6 +57,7 @@
|
||||
"react-native-dotenv": "^3.4.9",
|
||||
"react-native-dropdown-picker": "^5.4.6",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-modal": "^13.0.1",
|
||||
"react-native-reanimated": "^3.6.2",
|
||||
"react-native-safe-area-context": "^4.8.2",
|
||||
"react-native-screens": "~3.22.0",
|
||||
|
42
src/component/headers/back-header.tsx
Normal file
42
src/component/headers/back-header.tsx
Normal 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;
|
34
src/component/image/image-with-fallback.tsx
Normal file
34
src/component/image/image-with-fallback.tsx
Normal 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;
|
34
src/component/video-details/video-details-header.tsx
Normal file
34
src/component/video-details/video-details-header.tsx
Normal 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;
|
49
src/component/video-details/video-stat-list.tsx
Normal file
49
src/component/video-details/video-stat-list.tsx
Normal 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;
|
@ -4,7 +4,7 @@ import { Image } from "react-native";
|
||||
import CameraScreen from "../component/recording/camera";
|
||||
import Profile from "../screens/profile";
|
||||
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 { tabIconColors } from "../styles";
|
||||
|
||||
@ -70,7 +70,7 @@ export default function Tabs(): React.JSX.Element {
|
||||
<Tab.Screen
|
||||
name="VideoStack"
|
||||
component={VideoTabStack}
|
||||
options={{ tabBarLabel: "Video" }}
|
||||
options={{ tabBarLabel: "Feed" }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="RecordingStack"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
119
src/screens/video-stack/video-details.tsx
Normal file
119
src/screens/video-stack/video-details.tsx
Normal 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",
|
||||
},
|
||||
});
|
@ -1,6 +1,14 @@
|
||||
// GLOBAL STYLES
|
||||
import { StyleSheet } from "react-native";
|
||||
import Constants from "expo-constants";
|
||||
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({
|
||||
input: {
|
||||
width: "100%",
|
||||
@ -28,18 +36,21 @@ export const globalInputStyles = StyleSheet.create({
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
// COLORS:
|
||||
// can be made more granular to specify utility (ex: fontColors vs backgroundColors)
|
||||
export const colors = {
|
||||
bgBlack: "#121212",
|
||||
lightGrey: "#BFC2C8",
|
||||
darkGrey: "#767577",
|
||||
themeBrown: "#D9AA84",
|
||||
panelWhite: "#F2FBFE",
|
||||
tournamentBlue: "#50a6c2",
|
||||
blueCloth: "#539dc2",
|
||||
buttonBlue: "#1987ff",
|
||||
textWhite: "#ffffff",
|
||||
border: {
|
||||
grey: "#8F8E94",
|
||||
},
|
||||
text: {
|
||||
greyText: "#848580",
|
||||
},
|
||||
};
|
||||
|
||||
export const tabIconColors = {
|
||||
@ -47,7 +58,8 @@ export const tabIconColors = {
|
||||
selected: "#598EBB",
|
||||
};
|
||||
|
||||
export const shadows = {
|
||||
// SHADOWS
|
||||
export const shadows = StyleSheet.create({
|
||||
standard: {
|
||||
shadowColor: "#000000",
|
||||
shadowOffset: {
|
||||
@ -58,4 +70,47 @@ export const shadows = {
|
||||
shadowRadius: 4.65,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
@ -97,3 +97,14 @@ export const mockYData = [
|
||||
];
|
||||
|
||||
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...",
|
||||
};
|
||||
|
17
yarn.lock
17
yarn.lock
@ -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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
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"
|
||||
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:
|
||||
version "3.4.9"
|
||||
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"
|
||||
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:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.2.tgz#8a48c37251cbd3b665a659444fa9778f5b510356"
|
||||
|
Loading…
Reference in New Issue
Block a user