Merge pull request 'Actually respect prettier configuration' (#82) from ivan/respect-prettier-configuration into master
Reviewed-on: railbird/rn-playground#82
This commit is contained in:
commit
3f0e0bb9a9
@ -21,5 +21,7 @@ jobs:
|
|||||||
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run lint'
|
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run lint'
|
||||||
- name: typecheck
|
- name: typecheck
|
||||||
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn tsc --noEmit'
|
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn tsc --noEmit'
|
||||||
|
- name: prettier
|
||||||
|
run: nix develop --impure --command bash -c 'export HOME=$PWD; prettier . --check'
|
||||||
- name: test
|
- name: test
|
||||||
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run test --no-watchman'
|
run: nix develop --impure --command bash -c 'export HOME=$PWD; yarn run test --no-watchman'
|
||||||
|
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/android
|
||||||
|
/ios
|
||||||
|
/react-native-vision-camera
|
||||||
|
flake.lock
|
84
app.json
84
app.json
@ -1,44 +1,44 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Railbird",
|
"name": "Railbird",
|
||||||
"slug": "railbird-rn",
|
"slug": "railbird-rn",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "light",
|
"userInterfaceStyle": "light",
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash.png",
|
"image": "./assets/splash.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"assetBundlePatterns": ["**/*"],
|
"assetBundlePatterns": ["**/*"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@react-native-firebase/app",
|
"@react-native-firebase/app",
|
||||||
"@react-native-firebase/auth",
|
"@react-native-firebase/auth",
|
||||||
[
|
[
|
||||||
"expo-build-properties",
|
"expo-build-properties",
|
||||||
{
|
{
|
||||||
"ios": {
|
"ios": {
|
||||||
"useFrameworks": "static"
|
"useFrameworks": "static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"bundleIdentifier": "ai.railbird.railbird",
|
"bundleIdentifier": "ai.railbird.railbird",
|
||||||
"googleServicesFile": "./GoogleService-Info.plist"
|
"googleServicesFile": "./GoogleService-Info.plist"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"package": "android.railbird.app",
|
"package": "android.railbird.app",
|
||||||
"googleServicesFile": "./google-services.json"
|
"googleServicesFile": "./google-services.json"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,116 +1,137 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import * as scale from 'd3-scale';
|
import * as scale from "d3-scale";
|
||||||
import { View } from 'react-native';
|
import { View } from "react-native";
|
||||||
import { BarChart, XAxis, YAxis } from 'react-native-svg-charts';
|
import { BarChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||||
|
|
||||||
import { useGraphData } from '../use-graph-data';
|
import { useGraphData } from "../use-graph-data";
|
||||||
import { GraphData, YAxisProps } from '../graph-types';
|
import { GraphData, YAxisProps } from "../graph-types";
|
||||||
|
|
||||||
import { CustomBars } from '../custom-bars';
|
import { CustomBars } from "../custom-bars";
|
||||||
import { CustomGrid } from '../custom-grid';
|
import { CustomGrid } from "../custom-grid";
|
||||||
import { graphStyles } from '../chart-styles';
|
import { graphStyles } from "../chart-styles";
|
||||||
import ChartView from '../chart-view';
|
import ChartView from "../chart-view";
|
||||||
import ChartLabel from '../chart-label/chart-label';
|
import ChartLabel from "../chart-label/chart-label";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: GraphData;
|
data: GraphData;
|
||||||
height?: number;
|
height?: number;
|
||||||
spacingInner?: number;
|
spacingInner?: number;
|
||||||
spacingOuter?: number;
|
spacingOuter?: number;
|
||||||
contentInset?: { top: number; bottom: number };
|
contentInset?: { top: number; bottom: number };
|
||||||
min?: number;
|
min?: number;
|
||||||
numberOfTicks?: number;
|
numberOfTicks?: number;
|
||||||
barColors?: Array<string>;
|
barColors?: Array<string>;
|
||||||
useCommonScale?: boolean;
|
useCommonScale?: boolean;
|
||||||
yAxisProps?: YAxisProps;
|
yAxisProps?: YAxisProps;
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: separate PR will update useGraphData to take into account useCommonScale
|
// TODO: separate PR will update useGraphData to take into account useCommonScale
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
export const BarGraph: React.FC<Props> = ({ data, useCommonScale = false, testID, ...props }) => {
|
export const BarGraph: React.FC<Props> = ({
|
||||||
if (!data) {
|
data,
|
||||||
return null
|
useCommonScale = false,
|
||||||
} // TODO:#38
|
testID,
|
||||||
const {
|
...props
|
||||||
xValues,
|
}) => {
|
||||||
yData,
|
if (!data) {
|
||||||
yAxisRightLabels,
|
return null;
|
||||||
yAxisLeftLabels,
|
} // TODO:#38
|
||||||
defaultProps: {
|
const {
|
||||||
height,
|
xValues,
|
||||||
spacingInner,
|
yData,
|
||||||
spacingOuter,
|
yAxisRightLabels,
|
||||||
contentInset,
|
yAxisLeftLabels,
|
||||||
min,
|
defaultProps: {
|
||||||
numberOfTicks,
|
height,
|
||||||
barColors,
|
spacingInner,
|
||||||
yAxisProps: {
|
spacingOuter,
|
||||||
maxLeftYAxisValue,
|
contentInset,
|
||||||
maxRightYAxisValue,
|
min,
|
||||||
formatRightYAxisLabel,
|
numberOfTicks,
|
||||||
formatLeftYAxisLabel
|
barColors,
|
||||||
}
|
yAxisProps: {
|
||||||
}
|
maxLeftYAxisValue,
|
||||||
// Proper error/loading handling from useQueryHandler can work with this rule #38
|
maxRightYAxisValue,
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
formatRightYAxisLabel,
|
||||||
} = useGraphData(data, { includeColors: false, ...props});
|
formatLeftYAxisLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Proper error/loading handling from useQueryHandler can work with this rule #38
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
} = useGraphData(data, { includeColors: false, ...props });
|
||||||
|
|
||||||
// TODO: Will come from BE & destructured / color assigned in useGraphData
|
// TODO: Will come from BE & destructured / color assigned in useGraphData
|
||||||
const yLabels = [{ displayName: 'Shots Taken', axis: 'LEFT' as 'LEFT', color: barColors[0] }, { displayName:'Make Percentage', axis: 'RIGHT' as 'RIGHT', color: barColors[1] }]
|
const yLabels = [
|
||||||
const title = 'Shots Taken / Make Percentage by Cut Angle'
|
{ displayName: "Shots Taken", axis: "LEFT" as "LEFT", color: barColors[0] },
|
||||||
|
{
|
||||||
|
displayName: "Make Percentage",
|
||||||
|
axis: "RIGHT" as "RIGHT",
|
||||||
|
color: barColors[1],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const title = "Shots Taken / Make Percentage by Cut Angle";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartView>
|
<ChartView>
|
||||||
<ChartLabel title={title} yLabels={yLabels} />
|
<ChartLabel title={title} yLabels={yLabels} />
|
||||||
<View style={[graphStyles.rowContainer, { height: height }]} testID={`bar-graph-${testID}`}>
|
<View
|
||||||
<YAxis
|
style={[graphStyles.rowContainer, { height: height }]}
|
||||||
data={yAxisLeftLabels.values}
|
testID={`bar-graph-${testID}`}
|
||||||
contentInset={contentInset}
|
>
|
||||||
svg={graphStyles.yAxisFontStyle}
|
<YAxis
|
||||||
style={graphStyles.yAxisLeftPadding}
|
data={yAxisLeftLabels.values}
|
||||||
min={min}
|
contentInset={contentInset}
|
||||||
max={maxLeftYAxisValue}
|
svg={graphStyles.yAxisFontStyle}
|
||||||
numberOfTicks={numberOfTicks}
|
style={graphStyles.yAxisLeftPadding}
|
||||||
formatLabel={formatLeftYAxisLabel}
|
min={min}
|
||||||
/>
|
max={maxLeftYAxisValue}
|
||||||
|
numberOfTicks={numberOfTicks}
|
||||||
|
formatLabel={formatLeftYAxisLabel}
|
||||||
|
/>
|
||||||
|
|
||||||
<View style={graphStyles.flex}>
|
<View style={graphStyles.flex}>
|
||||||
<BarChart
|
<BarChart
|
||||||
style={graphStyles.flex}
|
style={graphStyles.flex}
|
||||||
data={yData}
|
data={yData}
|
||||||
gridMin={min}
|
gridMin={min}
|
||||||
numberOfTicks={numberOfTicks} // rethink numberOfTicks, it should be determined automatically if we do our y axis scaling properly
|
numberOfTicks={numberOfTicks} // rethink numberOfTicks, it should be determined automatically if we do our y axis scaling properly
|
||||||
yAccessor={({ item }) => (item as unknown as { value: number }).value}
|
yAccessor={({ item }) =>
|
||||||
contentInset={contentInset}
|
(item as unknown as { value: number }).value
|
||||||
spacingInner={spacingInner}
|
}
|
||||||
spacingOuter={spacingOuter}
|
contentInset={contentInset}
|
||||||
>
|
spacingInner={spacingInner}
|
||||||
<CustomGrid />
|
spacingOuter={spacingOuter}
|
||||||
<CustomBars barData={yData} xValues={xValues} barColors={barColors} />
|
>
|
||||||
</BarChart>
|
<CustomGrid />
|
||||||
<XAxis
|
<CustomBars
|
||||||
data={xValues.map((_, index) => index)}
|
barData={yData}
|
||||||
formatLabel={(_, index) => xValues[index]}
|
xValues={xValues}
|
||||||
style={graphStyles.xAxisMarginTop}
|
barColors={barColors}
|
||||||
svg={graphStyles.xAxisFontStyle}
|
/>
|
||||||
scale={scale.scaleBand}
|
</BarChart>
|
||||||
/>
|
<XAxis
|
||||||
</View>
|
data={xValues.map((_, index) => index)}
|
||||||
|
formatLabel={(_, index) => xValues[index]}
|
||||||
|
style={graphStyles.xAxisMarginTop}
|
||||||
|
svg={graphStyles.xAxisFontStyle}
|
||||||
|
scale={scale.scaleBand}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<YAxis
|
<YAxis
|
||||||
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
|
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
|
||||||
contentInset={contentInset}
|
contentInset={contentInset}
|
||||||
svg={graphStyles.yAxisFontStyle}
|
svg={graphStyles.yAxisFontStyle}
|
||||||
style={graphStyles.yAxisRightPadding}
|
style={graphStyles.yAxisRightPadding}
|
||||||
min={min}
|
min={min}
|
||||||
max={maxRightYAxisValue}
|
max={maxRightYAxisValue}
|
||||||
numberOfTicks={numberOfTicks}
|
numberOfTicks={numberOfTicks}
|
||||||
formatLabel={formatRightYAxisLabel}
|
formatLabel={formatRightYAxisLabel}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ChartView>
|
</ChartView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BarGraph;
|
export default BarGraph;
|
@ -1,48 +1,48 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Path } from 'react-native-svg';
|
import { Path } from "react-native-svg";
|
||||||
|
|
||||||
import { calculateBarOrigin, drawBarPath } from './custom-bar-utils';
|
import { calculateBarOrigin, drawBarPath } from "./custom-bar-utils";
|
||||||
import { ScaleBandType, ScaleLinearType, } from './graph-types';
|
import { ScaleBandType, ScaleLinearType } from "./graph-types";
|
||||||
|
|
||||||
interface BarProps {
|
interface BarProps {
|
||||||
scaleX: ScaleBandType;
|
scaleX: ScaleBandType;
|
||||||
scaleY: ScaleLinearType;
|
scaleY: ScaleLinearType;
|
||||||
data: { value: number };
|
data: { value: number };
|
||||||
barNumber: number;
|
barNumber: number;
|
||||||
index: number;
|
index: number;
|
||||||
fill: string;
|
fill: string;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
roundedRadius: number;
|
roundedRadius: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Bar: React.FC<BarProps> = ({
|
export const Bar: React.FC<BarProps> = ({
|
||||||
scaleX,
|
scaleX,
|
||||||
scaleY,
|
scaleY,
|
||||||
data,
|
data,
|
||||||
barNumber,
|
barNumber,
|
||||||
index,
|
index,
|
||||||
fill,
|
fill,
|
||||||
barWidth,
|
barWidth,
|
||||||
gap,
|
gap,
|
||||||
roundedRadius
|
roundedRadius,
|
||||||
}) => {
|
}) => {
|
||||||
const { xOrigin, yOrigin, height } = calculateBarOrigin({
|
const { xOrigin, yOrigin, height } = calculateBarOrigin({
|
||||||
scaleX,
|
scaleX,
|
||||||
scaleY,
|
scaleY,
|
||||||
index,
|
index,
|
||||||
data,
|
data,
|
||||||
barNumber,
|
barNumber,
|
||||||
barWidth,
|
barWidth,
|
||||||
gap
|
gap,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Path
|
<Path
|
||||||
key={`bar-path-${barNumber}-${index}`}
|
key={`bar-path-${barNumber}-${index}`}
|
||||||
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
|
d={drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius)}
|
||||||
fill={fill}
|
fill={fill}
|
||||||
testID={`bar-${barNumber}-${index}`}
|
testID={`bar-${barNumber}-${index}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,42 +1,44 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
import { chartLabel } from '../chart-styles';
|
import { chartLabel } from "../chart-styles";
|
||||||
|
|
||||||
type Axis = 'RIGHT' | 'LEFT'
|
type Axis = "RIGHT" | "LEFT";
|
||||||
|
|
||||||
interface YLabel {
|
interface YLabel {
|
||||||
axis: Axis;
|
axis: Axis;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartLabelProps = {
|
type ChartLabelProps = {
|
||||||
title: string;
|
title: string;
|
||||||
yLabels: Array<YLabel>;
|
yLabels: Array<YLabel>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLabels = (yLabels: Array<YLabel>) => {
|
const renderLabels = (yLabels: Array<YLabel>) => {
|
||||||
return yLabels.map((label) => (
|
return yLabels.map((label) => (
|
||||||
<View key={`${label.axis}-${label.displayName}`} style={chartLabel.labelInnerRow}>
|
<View
|
||||||
<View
|
key={`${label.axis}-${label.displayName}`}
|
||||||
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
|
style={chartLabel.labelInnerRow}
|
||||||
/>
|
>
|
||||||
<View style={chartLabel.labelTextMargin}>
|
<View
|
||||||
<Text style={chartLabel.labelText}>{label.displayName}</Text>
|
style={[chartLabel.labelColorBox, { backgroundColor: label.color }]}
|
||||||
</View>
|
/>
|
||||||
</View>
|
<View style={chartLabel.labelTextMargin}>
|
||||||
));
|
<Text style={chartLabel.labelText}>{label.displayName}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChartLabel({ title, yLabels }: ChartLabelProps) {
|
export default function ChartLabel({ title, yLabels }: ChartLabelProps) {
|
||||||
|
return (
|
||||||
return (
|
<View>
|
||||||
<View>
|
<View style={chartLabel.titleRow}>
|
||||||
<View style={chartLabel.titleRow}>
|
<Text style={chartLabel.titleText}>{title}</Text>
|
||||||
<Text style={chartLabel.titleText}>{title}</Text>
|
</View>
|
||||||
</View>
|
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
|
||||||
<View style={chartLabel.labelOuterRow}>{renderLabels(yLabels)}</View>
|
</View>
|
||||||
</View>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
@ -1,46 +1,46 @@
|
|||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
import { colors, shadows } from '../../styles';
|
import { colors, shadows } from "../../styles";
|
||||||
|
|
||||||
export const graphStyles = StyleSheet.create({
|
export const graphStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: colors.panelWhite,
|
backgroundColor: colors.panelWhite,
|
||||||
borderColor: 'black',
|
borderColor: "black",
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
marginHorizontal: 15,
|
marginHorizontal: 15,
|
||||||
paddingTop: 15,
|
paddingTop: 15,
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 15,
|
||||||
...shadows.standard
|
...shadows.standard,
|
||||||
},
|
},
|
||||||
rowContainer: { flexDirection: 'row', padding: 10 },
|
rowContainer: { flexDirection: "row", padding: 10 },
|
||||||
flex: { flex: 1 },
|
flex: { flex: 1 },
|
||||||
yAxisLeftPadding: { paddingRight: 3 },
|
yAxisLeftPadding: { paddingRight: 3 },
|
||||||
yAxisRightPadding: { paddingLeft: 3 },
|
yAxisRightPadding: { paddingLeft: 3 },
|
||||||
yAxisFontStyle: { fontSize: 10, fill: 'grey' },
|
yAxisFontStyle: { fontSize: 10, fill: "grey" },
|
||||||
xAxisFontStyle: { fontSize: 12, fill: 'black' },
|
xAxisFontStyle: { fontSize: 12, fill: "black" },
|
||||||
xAxisMarginTop: { marginTop: -15 },
|
xAxisMarginTop: { marginTop: -15 },
|
||||||
horizontalInset: { right: 10, left: 10 },
|
horizontalInset: { right: 10, left: 10 },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const chartLabel = StyleSheet.create({
|
export const chartLabel = StyleSheet.create({
|
||||||
titleRow: {
|
titleRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
marginHorizontal: 10
|
marginHorizontal: 10,
|
||||||
},
|
},
|
||||||
titleText: { fontWeight: '500' },
|
titleText: { fontWeight: "500" },
|
||||||
labelOuterRow: {
|
labelOuterRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
flexWrap: 'wrap',
|
flexWrap: "wrap",
|
||||||
marginHorizontal: 10
|
marginHorizontal: 10,
|
||||||
},
|
},
|
||||||
labelInnerRow: { flexDirection: 'row', alignItems: 'center', marginTop: 5 },
|
labelInnerRow: { flexDirection: "row", alignItems: "center", marginTop: 5 },
|
||||||
labelColorBox: {
|
labelColorBox: {
|
||||||
height: 15,
|
height: 15,
|
||||||
width: 15,
|
width: 15,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
marginRight: 2
|
marginRight: 2,
|
||||||
},
|
},
|
||||||
labelTextMargin: { marginRight: 15 },
|
labelTextMargin: { marginRight: 15 },
|
||||||
labelText: { fontSize: 12 }
|
labelText: { fontSize: 12 },
|
||||||
});
|
});
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { StyleProp, View, ViewStyle } from 'react-native';
|
import { StyleProp, View, ViewStyle } from "react-native";
|
||||||
|
|
||||||
import { graphStyles } from './chart-styles';
|
import { graphStyles } from "./chart-styles";
|
||||||
|
|
||||||
interface ChartViewProps {
|
interface ChartViewProps {
|
||||||
style?: StyleProp<ViewStyle>;
|
style?: StyleProp<ViewStyle>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChartView: React.FC<ChartViewProps> = ({ children, style, testID }) => {
|
const ChartView: React.FC<ChartViewProps> = ({ children, style, testID }) => {
|
||||||
return <View style={[graphStyles.container, style]} testID={testID} >{children}</View>;
|
return (
|
||||||
|
<View style={[graphStyles.container, style]} testID={testID}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChartView;
|
export default ChartView;
|
@ -1,59 +1,63 @@
|
|||||||
import { path as d3 } from 'd3-path';
|
import { path as d3 } from "d3-path";
|
||||||
|
|
||||||
import { ScaleLinearType, ScaleBandType } from './graph-types';
|
import { ScaleLinearType, ScaleBandType } from "./graph-types";
|
||||||
|
|
||||||
type BarCalculationProps = {
|
type BarCalculationProps = {
|
||||||
scaleX: ScaleBandType;
|
scaleX: ScaleBandType;
|
||||||
scaleY: ScaleLinearType;
|
scaleY: ScaleLinearType;
|
||||||
data: { value: number };
|
data: { value: number };
|
||||||
barNumber: number;
|
barNumber: number;
|
||||||
index: number;
|
index: number;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function calculateBarOrigin({
|
export function calculateBarOrigin({
|
||||||
scaleX,
|
scaleX,
|
||||||
scaleY,
|
scaleY,
|
||||||
barWidth,
|
barWidth,
|
||||||
data,
|
data,
|
||||||
index,
|
index,
|
||||||
barNumber,
|
barNumber,
|
||||||
gap
|
gap,
|
||||||
}: BarCalculationProps): { xOrigin: number; yOrigin: number; height: number } {
|
}: BarCalculationProps): { xOrigin: number; yOrigin: number; height: number } {
|
||||||
const firstBar = barNumber === 0;
|
const firstBar = barNumber === 0;
|
||||||
const xOrigin = scaleX(index) + (firstBar ? 0 : barWidth * barNumber + gap * barNumber);
|
const xOrigin =
|
||||||
const yOrigin = scaleY(data.value);
|
scaleX(index) + (firstBar ? 0 : barWidth * barNumber + gap * barNumber);
|
||||||
const height = scaleY(0) - yOrigin;
|
const yOrigin = scaleY(data.value);
|
||||||
|
const height = scaleY(0) - yOrigin;
|
||||||
|
|
||||||
return { xOrigin, yOrigin, height };
|
return { xOrigin, yOrigin, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function drawBarPath(
|
export function drawBarPath(
|
||||||
xOrigin: number,
|
xOrigin: number,
|
||||||
yOrigin: number,
|
yOrigin: number,
|
||||||
barWidth: number,
|
barWidth: number,
|
||||||
height: number,
|
height: number,
|
||||||
roundedRadius: number
|
roundedRadius: number,
|
||||||
): string {
|
): string {
|
||||||
const path = d3();
|
const path = d3();
|
||||||
path.moveTo(xOrigin, yOrigin + height);
|
path.moveTo(xOrigin, yOrigin + height);
|
||||||
path.lineTo(xOrigin, yOrigin + roundedRadius);
|
path.lineTo(xOrigin, yOrigin + roundedRadius);
|
||||||
path.arcTo(xOrigin, yOrigin, xOrigin + roundedRadius, yOrigin, roundedRadius);
|
path.arcTo(xOrigin, yOrigin, xOrigin + roundedRadius, yOrigin, roundedRadius);
|
||||||
path.lineTo(xOrigin + barWidth - roundedRadius, yOrigin);
|
path.lineTo(xOrigin + barWidth - roundedRadius, yOrigin);
|
||||||
path.arcTo(
|
path.arcTo(
|
||||||
xOrigin + barWidth,
|
xOrigin + barWidth,
|
||||||
yOrigin,
|
yOrigin,
|
||||||
xOrigin + barWidth,
|
xOrigin + barWidth,
|
||||||
yOrigin + roundedRadius,
|
yOrigin + roundedRadius,
|
||||||
roundedRadius
|
roundedRadius,
|
||||||
);
|
);
|
||||||
path.lineTo(xOrigin + barWidth, yOrigin + height);
|
path.lineTo(xOrigin + barWidth, yOrigin + height);
|
||||||
path.lineTo(xOrigin, yOrigin + height);
|
path.lineTo(xOrigin, yOrigin + height);
|
||||||
path.closePath();
|
path.closePath();
|
||||||
|
|
||||||
return path.toString();
|
return path.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calculateBarWidth = (bandwidth: number, combinedDataLength: number, gap: number) =>
|
export const calculateBarWidth = (
|
||||||
(bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;
|
bandwidth: number,
|
||||||
|
combinedDataLength: number,
|
||||||
|
gap: number,
|
||||||
|
) => (bandwidth - gap * (combinedDataLength - 1)) / combinedDataLength;
|
||||||
|
@ -1,54 +1,53 @@
|
|||||||
|
import { Svg } from "react-native-svg";
|
||||||
|
|
||||||
import { Svg } from 'react-native-svg';
|
import { Bar } from "./bar";
|
||||||
|
import { ScaleBandType, ScaleLinearType } from "./graph-types";
|
||||||
import { Bar } from './bar';
|
import { calculateBarWidth } from "./custom-bar-utils";
|
||||||
import { ScaleBandType, ScaleLinearType } from './graph-types';
|
|
||||||
import { calculateBarWidth } from './custom-bar-utils';
|
|
||||||
|
|
||||||
export interface CombinedData {
|
export interface CombinedData {
|
||||||
data: { value: number }[];
|
data: { value: number }[];
|
||||||
svg: { fill: string };
|
svg: { fill: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomBarsProps {
|
interface CustomBarsProps {
|
||||||
x: ScaleBandType;
|
x: ScaleBandType;
|
||||||
y: ScaleLinearType;
|
y: ScaleLinearType;
|
||||||
bandwidth: number;
|
bandwidth: number;
|
||||||
barColors: string[];
|
barColors: string[];
|
||||||
xValues: unknown[]; // TODO: update this value when this data type is defined
|
xValues: unknown[]; // TODO: update this value when this data type is defined
|
||||||
barData: CombinedData[];
|
barData: CombinedData[];
|
||||||
gap: number;
|
gap: number;
|
||||||
roundedRadius: number;
|
roundedRadius: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomBars: React.FC<Partial<CustomBarsProps>> = ({
|
export const CustomBars: React.FC<Partial<CustomBarsProps>> = ({
|
||||||
x: scaleX,
|
x: scaleX,
|
||||||
y: scaleY,
|
y: scaleY,
|
||||||
bandwidth,
|
bandwidth,
|
||||||
barData,
|
barData,
|
||||||
xValues,
|
xValues,
|
||||||
barColors,
|
barColors,
|
||||||
gap = 2,
|
gap = 2,
|
||||||
roundedRadius = 4
|
roundedRadius = 4,
|
||||||
}) => {
|
}) => {
|
||||||
const barWidth = calculateBarWidth(bandwidth, barData.length, gap);
|
const barWidth = calculateBarWidth(bandwidth, barData.length, gap);
|
||||||
|
|
||||||
return xValues.map((_, index) => (
|
return xValues.map((_, index) => (
|
||||||
<Svg key={`group-${index}`} testID={`svg-${index}`}>
|
<Svg key={`group-${index}`} testID={`svg-${index}`}>
|
||||||
{barData.map((item, i) => (
|
{barData.map((item, i) => (
|
||||||
<Bar
|
<Bar
|
||||||
key={`bar-${i}-${index}`}
|
key={`bar-${i}-${index}`}
|
||||||
scaleX={scaleX}
|
scaleX={scaleX}
|
||||||
scaleY={scaleY}
|
scaleY={scaleY}
|
||||||
data={item.data[index]}
|
data={item.data[index]}
|
||||||
barNumber={i}
|
barNumber={i}
|
||||||
index={index}
|
index={index}
|
||||||
fill={barColors[i]}
|
fill={barColors[i]}
|
||||||
barWidth={barWidth}
|
barWidth={barWidth}
|
||||||
gap={gap}
|
gap={gap}
|
||||||
roundedRadius={roundedRadius}
|
roundedRadius={roundedRadius}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Svg>
|
</Svg>
|
||||||
));
|
));
|
||||||
};
|
};
|
@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { G, Line } from 'react-native-svg';
|
import { G, Line } from "react-native-svg";
|
||||||
import { colors } from '../../styles';
|
import { colors } from "../../styles";
|
||||||
import { ScaleBandType, ScaleLinearType } from './graph-types';
|
import { ScaleBandType, ScaleLinearType } from "./graph-types";
|
||||||
|
|
||||||
interface CustomGridProps {
|
interface CustomGridProps {
|
||||||
x: ScaleBandType;
|
x: ScaleBandType;
|
||||||
y: ScaleLinearType;
|
y: ScaleLinearType;
|
||||||
ticks: Array<number>;
|
ticks: Array<number>;
|
||||||
includeVertical?: boolean;
|
includeVertical?: boolean;
|
||||||
xTicks?: Array<number | string>;
|
xTicks?: Array<number | string>;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* CustomGrid Component
|
* CustomGrid Component
|
||||||
@ -34,55 +34,74 @@ interface CustomGridProps {
|
|||||||
* Note: Use `includeVertical` cautiously; vertical lines are not fully developed.
|
* Note: Use `includeVertical` cautiously; vertical lines are not fully developed.
|
||||||
*/
|
*/
|
||||||
export const CustomGrid: React.FC<Partial<CustomGridProps>> = ({
|
export const CustomGrid: React.FC<Partial<CustomGridProps>> = ({
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
ticks,
|
ticks,
|
||||||
xTicks,
|
xTicks,
|
||||||
includeVertical = false
|
includeVertical = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [firstTick, ...remainingTicks] = ticks;
|
const [firstTick, ...remainingTicks] = ticks;
|
||||||
const dashArray = [1, 3];
|
const dashArray = [1, 3];
|
||||||
const strokeSolidWidth = 0.2;
|
const strokeSolidWidth = 0.2;
|
||||||
const strokeSolidColor = colors.bgBlack;
|
const strokeSolidColor = colors.bgBlack;
|
||||||
const strokeDashWidth = 1;
|
const strokeDashWidth = 1;
|
||||||
const strokeDashColor = colors.lightGrey;
|
const strokeDashColor = colors.lightGrey;
|
||||||
|
|
||||||
const renderHorizontalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => (
|
const renderHorizontalLine = (
|
||||||
<Line
|
tick: number,
|
||||||
key={`line-${tick}`}
|
stroke: string,
|
||||||
x1="0%"
|
strokeWidth: number,
|
||||||
x2="100%"
|
dashArray?: number[],
|
||||||
y1={y(tick)}
|
) => (
|
||||||
y2={y(tick)}
|
<Line
|
||||||
stroke={stroke}
|
key={`line-${tick}`}
|
||||||
strokeWidth={strokeWidth}
|
x1="0%"
|
||||||
strokeDasharray={dashArray}
|
x2="100%"
|
||||||
/>
|
y1={y(tick)}
|
||||||
);
|
y2={y(tick)}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={dashArray}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const topY = y(Math.max(...ticks));
|
const topY = y(Math.max(...ticks));
|
||||||
const bottomY = y(Math.min(...ticks));
|
const bottomY = y(Math.min(...ticks));
|
||||||
const renderVerticalLine = (tick: number, stroke: string, strokeWidth: number, dashArray?: number[]) => {
|
const renderVerticalLine = (
|
||||||
return (
|
tick: number,
|
||||||
<Line
|
stroke: string,
|
||||||
key={`vertical-line-${tick}`}
|
strokeWidth: number,
|
||||||
x1={x(tick)}
|
dashArray?: number[],
|
||||||
x2={x(tick)}
|
) => {
|
||||||
y1={topY}
|
return (
|
||||||
y2={bottomY}
|
<Line
|
||||||
stroke={stroke}
|
key={`vertical-line-${tick}`}
|
||||||
strokeWidth={strokeWidth}
|
x1={x(tick)}
|
||||||
strokeDasharray={dashArray}
|
x2={x(tick)}
|
||||||
/>
|
y1={topY}
|
||||||
);
|
y2={bottomY}
|
||||||
};
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={dashArray}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<G>
|
||||||
<G>
|
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
|
||||||
{renderHorizontalLine(firstTick, strokeSolidColor, strokeSolidWidth)}
|
{remainingTicks.map((tick) =>
|
||||||
{remainingTicks.map((tick) => renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray))}
|
renderHorizontalLine(tick, strokeDashColor, strokeDashWidth, dashArray),
|
||||||
{includeVertical && xTicks.map((_, index) => renderVerticalLine(index, strokeDashColor, strokeDashWidth, dashArray))}
|
)}
|
||||||
</G>
|
{includeVertical &&
|
||||||
);
|
xTicks.map((_, index) =>
|
||||||
|
renderVerticalLine(
|
||||||
|
index,
|
||||||
|
strokeDashColor,
|
||||||
|
strokeDashWidth,
|
||||||
|
dashArray,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</G>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
// TODO: Style values should be moved to styles
|
// TODO: Style values should be moved to styles
|
||||||
// non-style config can live here as chartDefaults
|
// non-style config can live here as chartDefaults
|
||||||
export const chartDefaults = {
|
export const chartDefaults = {
|
||||||
height: 300,
|
height: 300,
|
||||||
spacingInner: 0.3,
|
spacingInner: 0.3,
|
||||||
spacingOuter: 0.2,
|
spacingOuter: 0.2,
|
||||||
contentInset: { top: 30, bottom: 30 },
|
contentInset: { top: 30, bottom: 30 },
|
||||||
numberOfTicks: 6,
|
numberOfTicks: 6,
|
||||||
min: 0,
|
min: 0,
|
||||||
barColors: ['#598EBB', '#F2D4BC', '#DB7878'],
|
barColors: ["#598EBB", "#F2D4BC", "#DB7878"],
|
||||||
includeColors: true,
|
includeColors: true,
|
||||||
lineStrokeWidth: 2
|
lineStrokeWidth: 2,
|
||||||
};
|
};
|
@ -1,50 +1,50 @@
|
|||||||
import { ScaleBand, ScaleLinear } from 'd3-scale'
|
import { ScaleBand, ScaleLinear } from "d3-scale";
|
||||||
|
|
||||||
export type ScaleLinearType = ScaleLinear<number, number>;
|
export type ScaleLinearType = ScaleLinear<number, number>;
|
||||||
export type ScaleBandType = ScaleBand<number | string>;
|
export type ScaleBandType = ScaleBand<number | string>;
|
||||||
|
|
||||||
export interface YAxisData {
|
export interface YAxisData {
|
||||||
key: string; // string value for ChartLabel and useGraphData
|
key: string; // string value for ChartLabel and useGraphData
|
||||||
values: Array<number>;
|
values: Array<number>;
|
||||||
// including this code only for review --
|
// including this code only for review --
|
||||||
// do we prefer the idea of passing label formatting from the data or in the component?
|
// do we prefer the idea of passing label formatting from the data or in the component?
|
||||||
// generic function type, specific usage of value varies
|
// generic function type, specific usage of value varies
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
formatLabel?: (value: number) => string;
|
formatLabel?: (value: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type XValue = string | number
|
export type XValue = string | number;
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
xValues: Array<XValue>;
|
xValues: Array<XValue>;
|
||||||
yValues: Array<YAxisData>;
|
yValues: Array<YAxisData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YAxisProps {
|
export interface YAxisProps {
|
||||||
maxLeftYAxisValue?: number;
|
maxLeftYAxisValue?: number;
|
||||||
maxRightYAxisValue?: number;
|
maxRightYAxisValue?: number;
|
||||||
selectedLeftYAxisLabel?: string;
|
selectedLeftYAxisLabel?: string;
|
||||||
selectedRightYAxisLabel?: string;
|
selectedRightYAxisLabel?: string;
|
||||||
// generic function type, specific usage of value varies
|
// generic function type, specific usage of value varies
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
formatRightYAxisLabel?: (value: string) => string;
|
formatRightYAxisLabel?: (value: string) => string;
|
||||||
// generic function type, specific usage of value varies
|
// generic function type, specific usage of value varies
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
formatLeftYAxisLabel?: (value: string) => string;
|
formatLeftYAxisLabel?: (value: string) => string;
|
||||||
}
|
}
|
||||||
export interface GraphProps {
|
export interface GraphProps {
|
||||||
data: GraphData;
|
data: GraphData;
|
||||||
includeColors?: boolean;
|
includeColors?: boolean;
|
||||||
height?: number;
|
height?: number;
|
||||||
spacingInner?: number;
|
spacingInner?: number;
|
||||||
spacingOuter?: number;
|
spacingOuter?: number;
|
||||||
contentInset?: { top: number; bottom: number };
|
contentInset?: { top: number; bottom: number };
|
||||||
min?: number;
|
min?: number;
|
||||||
numberOfTicks?: number;
|
numberOfTicks?: number;
|
||||||
barColors?: Array<string>;
|
barColors?: Array<string>;
|
||||||
lineStrokeWidth?: number;
|
lineStrokeWidth?: number;
|
||||||
useCommonScale?: boolean;
|
useCommonScale?: boolean;
|
||||||
yAxisProps?: YAxisProps;
|
yAxisProps?: YAxisProps;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Common properties for graph render components.
|
* Common properties for graph render components.
|
||||||
@ -63,16 +63,16 @@ export interface GraphProps {
|
|||||||
* @property {string} [testID] - Optional. Identifier for testing purposes.
|
* @property {string} [testID] - Optional. Identifier for testing purposes.
|
||||||
*/
|
*/
|
||||||
export interface CommonProps {
|
export interface CommonProps {
|
||||||
data: GraphData;
|
data: GraphData;
|
||||||
height?: number;
|
height?: number;
|
||||||
spacingInner?: number;
|
spacingInner?: number;
|
||||||
spacingOuter?: number;
|
spacingOuter?: number;
|
||||||
contentInset?: { top: number; bottom: number };
|
contentInset?: { top: number; bottom: number };
|
||||||
min?: number;
|
min?: number;
|
||||||
numberOfTicks?: number;
|
numberOfTicks?: number;
|
||||||
barColors?: Array<string>;
|
barColors?: Array<string>;
|
||||||
lineStrokeWidth?: number;
|
lineStrokeWidth?: number;
|
||||||
useCommonScale?: boolean;
|
useCommonScale?: boolean;
|
||||||
yAxisProps?: YAxisProps;
|
yAxisProps?: YAxisProps;
|
||||||
testID?: string;
|
testID?: string;
|
||||||
}
|
}
|
@ -1,43 +1,52 @@
|
|||||||
import { GraphData } from "./graph-types";
|
import { GraphData } from "./graph-types";
|
||||||
|
|
||||||
|
|
||||||
export const convertToGraphData = (
|
export const convertToGraphData = (
|
||||||
graphData: GraphData,
|
graphData: GraphData,
|
||||||
options: {
|
options: {
|
||||||
selectedLeftYAxisLabel?: string;
|
selectedLeftYAxisLabel?: string;
|
||||||
selectedRightYAxisLabel?: string;
|
selectedRightYAxisLabel?: string;
|
||||||
maxRightYAxisValue: number;
|
maxRightYAxisValue: number;
|
||||||
maxLeftYAxisValue: number;
|
maxLeftYAxisValue: number;
|
||||||
includeColors: boolean;
|
includeColors: boolean;
|
||||||
barColors: Array<string>
|
barColors: Array<string>;
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
const xValues = graphData.xValues;
|
const xValues = graphData.xValues;
|
||||||
const leftAxisIndex = graphData.yValues.findIndex(
|
const leftAxisIndex = graphData.yValues.findIndex(
|
||||||
(y) => y.key === options.selectedLeftYAxisLabel
|
(y) => y.key === options.selectedLeftYAxisLabel,
|
||||||
);
|
);
|
||||||
const rightAxisIndex = graphData.yValues.findIndex(
|
const rightAxisIndex = graphData.yValues.findIndex(
|
||||||
(y) => y.key === options.selectedRightYAxisLabel
|
(y) => y.key === options.selectedRightYAxisLabel,
|
||||||
);
|
);
|
||||||
|
|
||||||
// scale data points according to max value
|
// scale data points according to max value
|
||||||
const yData = graphData.yValues.map((yAxis, index) => {
|
const yData = graphData.yValues.map((yAxis, index) => {
|
||||||
const maxValue = index === rightAxisIndex ? options.maxRightYAxisValue : options.maxLeftYAxisValue;
|
const maxValue =
|
||||||
|
index === rightAxisIndex
|
||||||
|
? options.maxRightYAxisValue
|
||||||
|
: options.maxLeftYAxisValue;
|
||||||
|
|
||||||
// scale values as a percentage of the max value
|
// scale values as a percentage of the max value
|
||||||
const scaledValues = yAxis.values.map((value) => (value / maxValue) * 100);
|
const scaledValues = yAxis.values.map((value) => (value / maxValue) * 100);
|
||||||
|
|
||||||
const strokeColor = options.includeColors && options.barColors ? options.barColors[index % options.barColors.length] : 'transparent';
|
const strokeColor =
|
||||||
|
options.includeColors && options.barColors
|
||||||
|
? options.barColors[index % options.barColors.length]
|
||||||
|
: "transparent";
|
||||||
|
|
||||||
const mappedData = scaledValues.map((scaledValue) => ({ value: scaledValue }));
|
const mappedData = scaledValues.map((scaledValue) => ({
|
||||||
|
value: scaledValue,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: mappedData,
|
data: mappedData,
|
||||||
svg: { fill: 'transparent', stroke: strokeColor } // Apply the stroke color here
|
svg: { fill: "transparent", stroke: strokeColor }, // Apply the stroke color here
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const yAxisLeftLabels = leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
|
const yAxisLeftLabels =
|
||||||
const yAxisRightLabels = rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
|
leftAxisIndex !== -1 ? graphData.yValues[leftAxisIndex] : null;
|
||||||
|
const yAxisRightLabels =
|
||||||
|
rightAxisIndex !== -1 ? graphData.yValues[rightAxisIndex] : undefined;
|
||||||
|
|
||||||
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
|
return { yData, yAxisLeftLabels, yAxisRightLabels, xValues };
|
||||||
};
|
};
|
||||||
|
@ -1,90 +1,103 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import * as shape from 'd3-shape';
|
import * as shape from "d3-shape";
|
||||||
import { View } from 'react-native';
|
import { View } from "react-native";
|
||||||
import { LineChart, XAxis, YAxis } from 'react-native-svg-charts';
|
import { LineChart, XAxis, YAxis } from "react-native-svg-charts";
|
||||||
|
|
||||||
import { useGraphData } from '../use-graph-data';
|
import { useGraphData } from "../use-graph-data";
|
||||||
import { CustomGrid } from '../custom-grid';
|
import { CustomGrid } from "../custom-grid";
|
||||||
import { graphStyles } from '../chart-styles';
|
import { graphStyles } from "../chart-styles";
|
||||||
import ChartView from '../chart-view';
|
import ChartView from "../chart-view";
|
||||||
import { CommonProps } from '../graph-types';
|
import { CommonProps } from "../graph-types";
|
||||||
|
|
||||||
// TODO: separate PR will update useGraphData to take into account useCommonScale
|
// TODO: separate PR will update useGraphData to take into account useCommonScale
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const LineGraph: React.FC<CommonProps> = ({ data, useCommonScale = false, testID, ...props }) => {
|
const LineGraph: React.FC<CommonProps> = ({
|
||||||
if (!data || typeof data !== 'object') {
|
data,
|
||||||
return null
|
useCommonScale = false,
|
||||||
}; // TODO:#38
|
testID,
|
||||||
const {
|
...props
|
||||||
xValues,
|
}) => {
|
||||||
yData,
|
if (!data || typeof data !== "object") {
|
||||||
yAxisLeftLabels,
|
return null;
|
||||||
yAxisRightLabels,
|
} // TODO:#38
|
||||||
defaultProps: {
|
const {
|
||||||
height,
|
xValues,
|
||||||
contentInset,
|
yData,
|
||||||
min,
|
yAxisLeftLabels,
|
||||||
numberOfTicks,
|
yAxisRightLabels,
|
||||||
lineStrokeWidth,
|
defaultProps: {
|
||||||
yAxisProps: {
|
height,
|
||||||
maxLeftYAxisValue,
|
contentInset,
|
||||||
maxRightYAxisValue,
|
min,
|
||||||
formatLeftYAxisLabel,
|
numberOfTicks,
|
||||||
formatRightYAxisLabel
|
lineStrokeWidth,
|
||||||
}
|
yAxisProps: {
|
||||||
}
|
maxLeftYAxisValue,
|
||||||
// Proper error/loading handling from useQueryHandler can work with this rule #38
|
maxRightYAxisValue,
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
formatLeftYAxisLabel,
|
||||||
} = useGraphData(data, props);
|
formatRightYAxisLabel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Proper error/loading handling from useQueryHandler can work with this rule #38
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
} = useGraphData(data, props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartView>
|
<ChartView>
|
||||||
<View style={[graphStyles.rowContainer, { height: height }]} testID={`line-graph-${testID}`}>
|
<View
|
||||||
<YAxis
|
style={[graphStyles.rowContainer, { height: height }]}
|
||||||
data={yAxisLeftLabels.values}
|
testID={`line-graph-${testID}`}
|
||||||
contentInset={contentInset}
|
>
|
||||||
svg={graphStyles.yAxisFontStyle}
|
<YAxis
|
||||||
style={graphStyles.yAxisLeftPadding}
|
data={yAxisLeftLabels.values}
|
||||||
min={min}
|
contentInset={contentInset}
|
||||||
max={maxLeftYAxisValue}
|
svg={graphStyles.yAxisFontStyle}
|
||||||
numberOfTicks={numberOfTicks}
|
style={graphStyles.yAxisLeftPadding}
|
||||||
formatLabel={formatLeftYAxisLabel}
|
min={min}
|
||||||
/>
|
max={maxLeftYAxisValue}
|
||||||
<View style={graphStyles.flex}>
|
numberOfTicks={numberOfTicks}
|
||||||
<LineChart
|
formatLabel={formatLeftYAxisLabel}
|
||||||
style={graphStyles.flex}
|
/>
|
||||||
data={yData}
|
<View style={graphStyles.flex}>
|
||||||
curve={shape.curveNatural}
|
<LineChart
|
||||||
svg={{ strokeWidth: lineStrokeWidth}}
|
style={graphStyles.flex}
|
||||||
yAccessor={({ item }) => (item as unknown as { value: number }).value}
|
data={yData}
|
||||||
xAccessor={({ index }) => xValues[index] as number}
|
curve={shape.curveNatural}
|
||||||
gridMin={min}
|
svg={{ strokeWidth: lineStrokeWidth }}
|
||||||
contentInset={contentInset}
|
yAccessor={({ item }) =>
|
||||||
numberOfTicks={numberOfTicks}
|
(item as unknown as { value: number }).value
|
||||||
>
|
}
|
||||||
<CustomGrid />
|
xAccessor={({ index }) => xValues[index] as number}
|
||||||
</LineChart>
|
gridMin={min}
|
||||||
<XAxis
|
contentInset={contentInset}
|
||||||
data={xValues.map((_, index: number) => index)} // TODO: update when useGraphHook returns explicit display values
|
numberOfTicks={numberOfTicks}
|
||||||
style={[graphStyles.xAxisMarginTop]}
|
>
|
||||||
svg={graphStyles.xAxisFontStyle}
|
<CustomGrid />
|
||||||
numberOfTicks={numberOfTicks}
|
</LineChart>
|
||||||
contentInset={graphStyles.horizontalInset}
|
<XAxis
|
||||||
/>
|
data={xValues.map((_, index: number) => index)} // TODO: update when useGraphHook returns explicit display values
|
||||||
</View>
|
style={[graphStyles.xAxisMarginTop]}
|
||||||
<YAxis
|
svg={graphStyles.xAxisFontStyle}
|
||||||
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
|
numberOfTicks={numberOfTicks}
|
||||||
contentInset={contentInset}
|
contentInset={graphStyles.horizontalInset}
|
||||||
svg={graphStyles.yAxisFontStyle}
|
/>
|
||||||
style={[graphStyles.yAxisRightPadding, { height: useCommonScale ? 0 : 'auto' }]}
|
</View>
|
||||||
min={min}
|
<YAxis
|
||||||
max={maxRightYAxisValue}
|
data={yAxisRightLabels?.values ?? yAxisLeftLabels.values}
|
||||||
numberOfTicks={numberOfTicks}
|
contentInset={contentInset}
|
||||||
formatLabel={formatRightYAxisLabel} // formatRightYAxisLabel formatting could come from yAxisRightLabels object
|
svg={graphStyles.yAxisFontStyle}
|
||||||
/>
|
style={[
|
||||||
</View>
|
graphStyles.yAxisRightPadding,
|
||||||
</ChartView>
|
{ height: useCommonScale ? 0 : "auto" },
|
||||||
);
|
]}
|
||||||
|
min={min}
|
||||||
|
max={maxRightYAxisValue}
|
||||||
|
numberOfTicks={numberOfTicks}
|
||||||
|
formatLabel={formatRightYAxisLabel} // formatRightYAxisLabel formatting could come from yAxisRightLabels object
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ChartView>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LineGraph;
|
export default LineGraph;
|
||||||
|
@ -1,57 +1,70 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { convertToGraphData } from './graph-utils';
|
|
||||||
import { chartDefaults } from './graph-config';
|
|
||||||
import { GraphData, GraphProps, XValue, YAxisData } from './graph-types';
|
|
||||||
|
|
||||||
|
|
||||||
|
import { convertToGraphData } from "./graph-utils";
|
||||||
|
import { chartDefaults } from "./graph-config";
|
||||||
|
import { GraphData, GraphProps, XValue, YAxisData } from "./graph-types";
|
||||||
|
|
||||||
interface useGraphDataInterface {
|
interface useGraphDataInterface {
|
||||||
xValues: Array<XValue>;
|
xValues: Array<XValue>;
|
||||||
yData: {
|
yData: {
|
||||||
data: {
|
data: {
|
||||||
value: number;
|
value: number;
|
||||||
}[];
|
}[];
|
||||||
svg: {
|
svg: {
|
||||||
fill: string;
|
fill: string;
|
||||||
};
|
};
|
||||||
}[];
|
}[];
|
||||||
yAxisLeftLabels: YAxisData;
|
yAxisLeftLabels: YAxisData;
|
||||||
yAxisRightLabels: YAxisData;
|
yAxisRightLabels: YAxisData;
|
||||||
defaultProps: Partial<GraphProps>;
|
defaultProps: Partial<GraphProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this version assumes string values for X, this isn't necessarily the case
|
// this version assumes string values for X, this isn't necessarily the case
|
||||||
// convertToGraphData is specifically tailored to bar/group bar graphs
|
// convertToGraphData is specifically tailored to bar/group bar graphs
|
||||||
// ultimately this component could be used by any x & y axis graph types (line/bar/scatter)
|
// ultimately this component could be used by any x & y axis graph types (line/bar/scatter)
|
||||||
export const useGraphData = (graphData: GraphData, props: Partial<GraphProps>): useGraphDataInterface => {
|
export const useGraphData = (
|
||||||
const { yAxisProps = {}, ...otherProps } = props;
|
graphData: GraphData,
|
||||||
const defaultProps = {
|
props: Partial<GraphProps>,
|
||||||
...chartDefaults,
|
): useGraphDataInterface => {
|
||||||
...otherProps,
|
const { yAxisProps = {}, ...otherProps } = props;
|
||||||
// assign default values for yAxisProps + spread to override with values coming from props
|
const defaultProps = {
|
||||||
yAxisProps: {
|
...chartDefaults,
|
||||||
maxLeftYAxisValue: Math.max(...(graphData.yValues[0]?.values ?? [0])),
|
...otherProps,
|
||||||
maxRightYAxisValue:
|
// assign default values for yAxisProps + spread to override with values coming from props
|
||||||
graphData.yValues.length > 1 ? Math.max(...graphData.yValues[1]?.values) : undefined,
|
yAxisProps: {
|
||||||
formatRightYAxisLabel: yAxisProps.formatRightYAxisLabel,
|
maxLeftYAxisValue: Math.max(...(graphData.yValues[0]?.values ?? [0])),
|
||||||
formatLeftYAxisLabel: yAxisProps.formatLeftYAxisLabel,
|
maxRightYAxisValue:
|
||||||
selectedLeftYAxisLabel: graphData.yValues[0]?.key,
|
graphData.yValues.length > 1
|
||||||
selectedRightYAxisLabel: graphData.yValues[1]?.key,
|
? Math.max(...graphData.yValues[1]?.values)
|
||||||
...yAxisProps
|
: undefined,
|
||||||
}
|
formatRightYAxisLabel: yAxisProps.formatRightYAxisLabel,
|
||||||
};
|
formatLeftYAxisLabel: yAxisProps.formatLeftYAxisLabel,
|
||||||
|
selectedLeftYAxisLabel: graphData.yValues[0]?.key,
|
||||||
|
selectedRightYAxisLabel: graphData.yValues[1]?.key,
|
||||||
|
...yAxisProps,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
|
const { yData, yAxisLeftLabels, yAxisRightLabels, xValues } = useMemo(
|
||||||
() => convertToGraphData(graphData, { ...defaultProps.yAxisProps, includeColors: defaultProps.includeColors, barColors: defaultProps.barColors}),
|
() =>
|
||||||
[graphData, defaultProps.yAxisProps, defaultProps.includeColors, defaultProps.barColors]
|
convertToGraphData(graphData, {
|
||||||
);
|
...defaultProps.yAxisProps,
|
||||||
|
includeColors: defaultProps.includeColors,
|
||||||
|
barColors: defaultProps.barColors,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
graphData,
|
||||||
|
defaultProps.yAxisProps,
|
||||||
|
defaultProps.includeColors,
|
||||||
|
defaultProps.barColors,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
xValues,
|
xValues,
|
||||||
yData,
|
yData,
|
||||||
yAxisLeftLabels,
|
yAxisLeftLabels,
|
||||||
yAxisRightLabels,
|
yAxisRightLabels,
|
||||||
defaultProps
|
defaultProps,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,169 +1,177 @@
|
|||||||
import React, { useCallback, useRef, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { Button, StyleSheet, Text, View } from "react-native";
|
import { Button, StyleSheet, Text, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
Camera,
|
Camera,
|
||||||
useCameraPermission,
|
useCameraPermission,
|
||||||
useCameraDevice,
|
useCameraDevice,
|
||||||
useCameraFormat,
|
useCameraFormat,
|
||||||
PhotoFile,
|
PhotoFile,
|
||||||
VideoFile,
|
VideoFile,
|
||||||
CameraRuntimeError,
|
CameraRuntimeError,
|
||||||
Orientation,
|
Orientation,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from "react-native-vision-camera";
|
} from "react-native-vision-camera";
|
||||||
import { RecordingButton } from "./capture-button";
|
import { RecordingButton } from "./capture-button";
|
||||||
import { useIsForeground } from "./is-foreground";
|
import { useIsForeground } from "./is-foreground";
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
|
|
||||||
export default function CameraScreen({ route, navigation }): React.ReactElement {
|
export default function CameraScreen({
|
||||||
// TODO: #73 Does this need to be passed to Camera component?
|
route,
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
navigation,
|
||||||
const { gameType, tableSize, tags, location } = route.params
|
}): React.ReactElement {
|
||||||
// LOG for params -- Remove when no longer needed
|
// TODO: #73 Does this need to be passed to Camera component?
|
||||||
// Note: camelCased value being passed, change on record.tsx if you want a different value format
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
console.log(gameType, tableSize, tags, location)
|
const { gameType, tableSize, tags, location } = route.params;
|
||||||
|
// LOG for params -- Remove when no longer needed
|
||||||
|
// Note: camelCased value being passed, change on record.tsx if you want a different value format
|
||||||
|
console.log(gameType, tableSize, tags, location);
|
||||||
|
|
||||||
|
const camera = useRef<Camera>(null);
|
||||||
|
const { hasPermission, requestPermission } = useCameraPermission();
|
||||||
|
const [isCameraInitialized, setIsCameraInitialized] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const camera = useRef<Camera>(null);
|
const isForeground = useIsForeground();
|
||||||
const { hasPermission, requestPermission } = useCameraPermission();
|
const isFocused = useIsFocused();
|
||||||
const [isCameraInitialized, setIsCameraInitialized] =
|
const isActive = isForeground && isFocused;
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const isForeground = useIsForeground();
|
const onError = useCallback((error: CameraRuntimeError) => {
|
||||||
const isFocused = useIsFocused();
|
console.error(error);
|
||||||
const isActive = isForeground && isFocused;
|
}, []);
|
||||||
|
|
||||||
const onError = useCallback((error: CameraRuntimeError) => {
|
const onInitialized = useCallback(() => {
|
||||||
console.error(error);
|
console.log("Camera initialized!");
|
||||||
}, []);
|
setIsCameraInitialized(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onInitialized = useCallback(() => {
|
const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => {
|
||||||
console.log("Camera initialized!");
|
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||||
setIsCameraInitialized(true);
|
}, []);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => {
|
const onVideoChunkReady = useCallback((event) => {
|
||||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
console.log(`Chunk ready in react-native`, event.nativeEvent);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onVideoChunkReady = useCallback((event) => {
|
if (!hasPermission) {
|
||||||
console.log(`Chunk ready in react-native`, event.nativeEvent);
|
requestPermission();
|
||||||
}, []);
|
// Error handling in case they refuse to give permission
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasPermission) {
|
const device = useCameraDevice("back");
|
||||||
requestPermission();
|
const format = useCameraFormat(device, [
|
||||||
// Error handling in case they refuse to give permission
|
{ videoResolution: { width: 3048, height: 2160 } },
|
||||||
}
|
{ fps: 60 },
|
||||||
|
]); // this sets as a target
|
||||||
|
|
||||||
const device = useCameraDevice("back");
|
//Orientation detection
|
||||||
const format = useCameraFormat(device, [
|
const [orientation, setOrientation] = useState<Orientation>("portrait");
|
||||||
{ videoResolution: { width: 3048, height: 2160 } },
|
|
||||||
{ fps: 60 },
|
|
||||||
]); // this sets as a target
|
|
||||||
|
|
||||||
//Orientation detection
|
const toggleOrientation = () => {
|
||||||
const [orientation, setOrientation] = useState<Orientation>("portrait");
|
setOrientation(
|
||||||
|
(currentOrientation) =>
|
||||||
|
currentOrientation === "landscape-left" ? "portrait" : "landscape-left", // Can adjust this and the type to match what we want
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleOrientation = () => {
|
// Replace with error handling
|
||||||
setOrientation(
|
if (device === null) {
|
||||||
(currentOrientation) =>
|
console.log(device);
|
||||||
currentOrientation === "landscape-left" ? "portrait" : "landscape-left", // Can adjust this and the type to match what we want
|
return (
|
||||||
);
|
<Text>
|
||||||
};
|
Camera not available. Does user have permissions: {hasPermission}
|
||||||
|
</Text>
|
||||||
// Replace with error handling
|
);
|
||||||
if (device === null) {
|
}
|
||||||
console.log(device);
|
return (
|
||||||
return (
|
hasPermission && (
|
||||||
<Text>
|
<View style={styles.container}>
|
||||||
Camera not available. Does user have permissions: {hasPermission}
|
<Camera
|
||||||
</Text>
|
ref={camera}
|
||||||
);
|
style={StyleSheet.absoluteFill}
|
||||||
}
|
device={device}
|
||||||
return (
|
format={format}
|
||||||
hasPermission && (
|
onInitialized={onInitialized}
|
||||||
<View style={styles.container}>
|
onError={onError}
|
||||||
<Camera
|
onVideoChunkReady={onVideoChunkReady}
|
||||||
ref={camera}
|
video={true}
|
||||||
style={StyleSheet.absoluteFill}
|
orientation={orientation} // TODO: #60
|
||||||
device={device}
|
isActive={isActive}
|
||||||
format={format}
|
/>
|
||||||
onInitialized={onInitialized}
|
<View
|
||||||
onError={onError}
|
style={
|
||||||
onVideoChunkReady={onVideoChunkReady}
|
orientation === "portrait"
|
||||||
video={true}
|
? styles.goBackPortrait
|
||||||
orientation={orientation} // TODO: #60
|
: styles.goBackLandscape
|
||||||
isActive={isActive}
|
}
|
||||||
/>
|
>
|
||||||
<View style={orientation === "portrait" ? styles.goBackPortrait : styles.goBackLandscape}>
|
<Button title="Go back" onPress={() => navigation.goBack()} />
|
||||||
<Button title="Go back" onPress={() => navigation.goBack()} />
|
</View>
|
||||||
</View>
|
<RecordingButton
|
||||||
<RecordingButton
|
style={[
|
||||||
style={[
|
styles.captureButton,
|
||||||
styles.captureButton,
|
orientation === "portrait" ? styles.portrait : styles.landscape,
|
||||||
orientation === "portrait" ? styles.portrait : styles.landscape,
|
]}
|
||||||
]}
|
camera={camera}
|
||||||
camera={camera}
|
onMediaCaptured={onMediaCaptured}
|
||||||
onMediaCaptured={onMediaCaptured}
|
enabled={isCameraInitialized}
|
||||||
enabled={isCameraInitialized}
|
/>
|
||||||
/>
|
<View
|
||||||
<View
|
style={[
|
||||||
style={[
|
styles.button,
|
||||||
styles.button,
|
orientation === "portrait"
|
||||||
orientation === "portrait"
|
? styles.togglePortrait
|
||||||
? styles.togglePortrait
|
: styles.toggleLandscape,
|
||||||
: styles.toggleLandscape,
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<Button
|
||||||
<Button
|
title="Toggle Orientation"
|
||||||
title="Toggle Orientation"
|
onPress={toggleOrientation}
|
||||||
onPress={toggleOrientation}
|
color="#841584"
|
||||||
color="#841584"
|
accessibilityLabel="Toggle camera orientation"
|
||||||
accessibilityLabel="Toggle camera orientation"
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)
|
||||||
)
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
},
|
},
|
||||||
captureButton: {
|
captureButton: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
},
|
},
|
||||||
togglePortrait: {
|
togglePortrait: {
|
||||||
bottom: 110, // needs refined
|
bottom: 110, // needs refined
|
||||||
},
|
},
|
||||||
toggleLandscape: {
|
toggleLandscape: {
|
||||||
transform: [{ rotate: "90deg" }],
|
transform: [{ rotate: "90deg" }],
|
||||||
bottom: "43%", // Should come from SafeAreaProvider, hardcoded right now, should roughly appear above the button
|
bottom: "43%", // Should come from SafeAreaProvider, hardcoded right now, should roughly appear above the button
|
||||||
left: 50, // needs refined
|
left: 50, // needs refined
|
||||||
},
|
},
|
||||||
portrait: {
|
portrait: {
|
||||||
bottom: 20, // needs refined
|
bottom: 20, // needs refined
|
||||||
},
|
},
|
||||||
landscape: {
|
landscape: {
|
||||||
bottom: "40%", // Should come from SafeAreaProvider
|
bottom: "40%", // Should come from SafeAreaProvider
|
||||||
left: 20, // needs refined
|
left: 20, // needs refined
|
||||||
},
|
},
|
||||||
goBackPortrait: {
|
goBackPortrait: {
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 20, // or wherever you want the button to be positioned in portrait
|
top: 20, // or wherever you want the button to be positioned in portrait
|
||||||
left: 20, // or wherever you want the button to be positioned in portrait
|
left: 20, // or wherever you want the button to be positioned in portrait
|
||||||
},
|
},
|
||||||
goBackLandscape: {
|
goBackLandscape: {
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
top: 40,
|
top: 40,
|
||||||
right: 20,
|
right: 20,
|
||||||
transform: [{ rotate: '90deg' }],
|
transform: [{ rotate: "90deg" }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
import { Dimensions, Platform } from 'react-native'
|
import { Dimensions, Platform } from "react-native";
|
||||||
import StaticSafeAreaInsets from 'react-native-static-safe-area-insets'
|
import StaticSafeAreaInsets from "react-native-static-safe-area-insets";
|
||||||
|
|
||||||
export const CONTENT_SPACING = 15
|
export const CONTENT_SPACING = 15;
|
||||||
|
|
||||||
const SAFE_BOTTOM =
|
const SAFE_BOTTOM =
|
||||||
Platform.select({
|
Platform.select({
|
||||||
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
|
ios: StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||||
}) ?? 0
|
}) ?? 0;
|
||||||
|
|
||||||
export const SAFE_AREA_PADDING = {
|
export const SAFE_AREA_PADDING = {
|
||||||
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
|
paddingLeft: StaticSafeAreaInsets.safeAreaInsetsLeft + CONTENT_SPACING,
|
||||||
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
|
paddingTop: StaticSafeAreaInsets.safeAreaInsetsTop + CONTENT_SPACING,
|
||||||
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
|
paddingRight: StaticSafeAreaInsets.safeAreaInsetsRight + CONTENT_SPACING,
|
||||||
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
|
paddingBottom: SAFE_BOTTOM + CONTENT_SPACING,
|
||||||
}
|
};
|
||||||
|
|
||||||
// The maximum zoom _factor_ you should be able to zoom in
|
// The maximum zoom _factor_ you should be able to zoom in
|
||||||
export const MAX_ZOOM_FACTOR = 10
|
export const MAX_ZOOM_FACTOR = 10;
|
||||||
|
|
||||||
export const SCREEN_WIDTH = Dimensions.get('window').width
|
export const SCREEN_WIDTH = Dimensions.get("window").width;
|
||||||
export const SCREEN_HEIGHT = Platform.select<number>({
|
export const SCREEN_HEIGHT = Platform.select<number>({
|
||||||
android: Dimensions.get('screen').height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
android:
|
||||||
ios: Dimensions.get('window').height,
|
Dimensions.get("screen").height - StaticSafeAreaInsets.safeAreaInsetsBottom,
|
||||||
}) as number
|
ios: Dimensions.get("window").height,
|
||||||
|
}) as number;
|
||||||
|
|
||||||
// Capture Button
|
// Capture Button
|
||||||
export const CAPTURE_BUTTON_SIZE = 78
|
export const CAPTURE_BUTTON_SIZE = 78;
|
||||||
|
|
||||||
// Control Button like Flash
|
// Control Button like Flash
|
||||||
export const CONTROL_BUTTON_SIZE = 40
|
export const CONTROL_BUTTON_SIZE = 40;
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from "react";
|
||||||
import { AppState, AppStateStatus } from 'react-native'
|
import { AppState, AppStateStatus } from "react-native";
|
||||||
|
|
||||||
export const useIsForeground = (): boolean => {
|
export const useIsForeground = (): boolean => {
|
||||||
const [isForeground, setIsForeground] = useState(true)
|
const [isForeground, setIsForeground] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onChange = (state: AppStateStatus): void => {
|
const onChange = (state: AppStateStatus): void => {
|
||||||
setIsForeground(state === 'active')
|
setIsForeground(state === "active");
|
||||||
}
|
};
|
||||||
const listener = AppState.addEventListener('change', onChange)
|
const listener = AppState.addEventListener("change", onChange);
|
||||||
return () => listener.remove()
|
return () => listener.remove();
|
||||||
}, [setIsForeground])
|
}, [setIsForeground]);
|
||||||
|
|
||||||
return isForeground
|
return isForeground;
|
||||||
}
|
};
|
||||||
|
@ -1,47 +1,47 @@
|
|||||||
{
|
{
|
||||||
"project_info": {
|
"project_info": {
|
||||||
"project_number": "735905563616",
|
"project_number": "735905563616",
|
||||||
"project_id": "railbird-infra",
|
"project_id": "railbird-infra",
|
||||||
"storage_bucket": "railbird-infra.appspot.com"
|
"storage_bucket": "railbird-infra.appspot.com"
|
||||||
},
|
},
|
||||||
"client": [
|
"client": [
|
||||||
{
|
{
|
||||||
"client_info": {
|
"client_info": {
|
||||||
"mobilesdk_app_id": "1:735905563616:android:7eefd99f68d2f7db702185",
|
"mobilesdk_app_id": "1:735905563616:android:7eefd99f68d2f7db702185",
|
||||||
"android_client_info": {
|
"android_client_info": {
|
||||||
"package_name": "android.railbird.app"
|
"package_name": "android.railbird.app"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
{
|
{
|
||||||
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
|
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"api_key": [
|
"api_key": [
|
||||||
{
|
{
|
||||||
"current_key": "AIzaSyA5o4LpiDFl8Q8AaA2eGjbgdS7tMfupCWg"
|
"current_key": "AIzaSyA5o4LpiDFl8Q8AaA2eGjbgdS7tMfupCWg"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": {
|
"services": {
|
||||||
"appinvite_service": {
|
"appinvite_service": {
|
||||||
"other_platform_oauth_client": [
|
"other_platform_oauth_client": [
|
||||||
{
|
{
|
||||||
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
|
"client_id": "735905563616-v12rcdm7pkm1r2t5v09th5bs9j3ah1e4.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": "735905563616-ncd8794ocn2f25qmnaascn88upfgokp0.apps.googleusercontent.com",
|
"client_id": "735905563616-ncd8794ocn2f25qmnaascn88upfgokp0.apps.googleusercontent.com",
|
||||||
"client_type": 2,
|
"client_type": 2,
|
||||||
"ios_info": {
|
"ios_info": {
|
||||||
"bundle_id": "ai.railbird.railbird",
|
"bundle_id": "ai.railbird.railbird",
|
||||||
"app_store_id": "6469274937"
|
"app_store_id": "6469274937"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configuration_version": "1"
|
"configuration_version": "1"
|
||||||
}
|
}
|
4
index.js
4
index.js
@ -1,6 +1,6 @@
|
|||||||
import { registerRootComponent } from 'expo';
|
import { registerRootComponent } from "expo";
|
||||||
|
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
|
|
||||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
// It also ensures that whether you load the app in Expo Go or in a native build,
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
@ -1,64 +1,64 @@
|
|||||||
export const graph_data_one_measures = {
|
export const graph_data_one_measures = {
|
||||||
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
|
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
|
||||||
yValues: [
|
yValues: [
|
||||||
{
|
{
|
||||||
key: 'measure_1',
|
key: "measure_1",
|
||||||
values: [100, 140, 90, 80, 40]
|
values: [100, 140, 90, 80, 40],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
export const graph_data_two_measures = {
|
export const graph_data_two_measures = {
|
||||||
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
|
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
|
||||||
yValues: [
|
yValues: [
|
||||||
{
|
{
|
||||||
key: 'measure_1',
|
key: "measure_1",
|
||||||
values: [100, 140, 90, 80, 40]
|
values: [100, 140, 90, 80, 40],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'measure_2',
|
key: "measure_2",
|
||||||
values: [78, 82, 73, 56, 61],
|
values: [78, 82, 73, 56, 61],
|
||||||
formatLabel: (value: number) => `${value}%`
|
formatLabel: (value: number) => `${value}%`,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
export const graph_data_three_measures = {
|
export const graph_data_three_measures = {
|
||||||
xValues: ['x_1', 'x_2', 'x_3', 'x_4', 'x_5'],
|
xValues: ["x_1", "x_2", "x_3", "x_4", "x_5"],
|
||||||
yValues: [
|
yValues: [
|
||||||
{
|
{
|
||||||
key: 'measure_1',
|
key: "measure_1",
|
||||||
values: [100, 140, 90, 80, 40]
|
values: [100, 140, 90, 80, 40],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'measure_2',
|
key: "measure_2",
|
||||||
values: [78, 82, 73, 56, 61],
|
values: [78, 82, 73, 56, 61],
|
||||||
formatLabel: (value: number) => `${value}%`
|
formatLabel: (value: number) => `${value}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'measure_3',
|
key: "measure_3",
|
||||||
values: [77, 32, 45, 65, 50]
|
values: [77, 32, 45, 65, 50],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const line_chart_one_y_data = {
|
export const line_chart_one_y_data = {
|
||||||
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
yValues: [
|
yValues: [
|
||||||
{
|
{
|
||||||
key: 'measure_1',
|
key: "measure_1",
|
||||||
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30]
|
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
export const line_chart_two_y_data = {
|
export const line_chart_two_y_data = {
|
||||||
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||||
yValues: [
|
yValues: [
|
||||||
{
|
{
|
||||||
key: 'measure_1',
|
key: "measure_1",
|
||||||
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30]
|
values: [100, 140, 90, 80, 40, 20, 70, 20, 30, 30],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'measure_2',
|
key: "measure_2",
|
||||||
values: [50, 67, 123, 140, 156, 147, 126, 180, 123, 87]
|
values: [50, 67, 123, 140, 156, 147, 126, 180, 123, 87],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
};
|
};
|
@ -1,26 +1,30 @@
|
|||||||
import { useColorScheme } from 'react-native';
|
import { useColorScheme } from "react-native";
|
||||||
import { NavigationContainer, DarkTheme, DefaultTheme } from '@react-navigation/native';
|
import {
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
NavigationContainer,
|
||||||
|
DarkTheme,
|
||||||
|
DefaultTheme,
|
||||||
|
} from "@react-navigation/native";
|
||||||
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||||
|
|
||||||
import Tabs from './tab-navigator';
|
import Tabs from "./tab-navigator";
|
||||||
import Login from '../screens/login';
|
import Login from "../screens/login";
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
const ScreensStack = () => (
|
const ScreensStack = () => (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Tabs"
|
name="Tabs"
|
||||||
component={Tabs}
|
component={Tabs}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="Login"
|
name="Login"
|
||||||
component={Login}
|
component={Login}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
)
|
);
|
||||||
/**
|
/**
|
||||||
* Functional component for app navigation. Configures a navigation container with a stack navigator.
|
* Functional component for app navigation. Configures a navigation container with a stack navigator.
|
||||||
* Dynamically selects between dark and light themes based on the device's color scheme.
|
* Dynamically selects between dark and light themes based on the device's color scheme.
|
||||||
@ -29,15 +33,14 @@ const ScreensStack = () => (
|
|||||||
* @returns {React.ComponentType} A NavigationContainer wrapping a Stack.Navigator for app screens.
|
* @returns {React.ComponentType} A NavigationContainer wrapping a Stack.Navigator for app screens.
|
||||||
*/
|
*/
|
||||||
export default function AppNavigator(): React.JSX.Element {
|
export default function AppNavigator(): React.JSX.Element {
|
||||||
|
// useColorScheme get's the theme from device settings
|
||||||
|
const scheme = useColorScheme();
|
||||||
|
|
||||||
// useColorScheme get's the theme from device settings
|
return (
|
||||||
const scheme = useColorScheme();
|
<NavigationContainer theme={scheme === "dark" ? DarkTheme : DefaultTheme}>
|
||||||
|
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
return (
|
<Stack.Screen name="App" component={ScreensStack} />
|
||||||
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
|
</Stack.Navigator>
|
||||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
</NavigationContainer>
|
||||||
<Stack.Screen name="App" component={ScreensStack} />
|
);
|
||||||
</Stack.Navigator>
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
}
|
}
|
@ -1,35 +1,29 @@
|
|||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||||
import { Image } from 'react-native';
|
import { Image } from "react-native";
|
||||||
import CameraScreen from '../component/video/camera';
|
import CameraScreen from "../component/video/camera";
|
||||||
import Session from '../screens/session';
|
import Session from "../screens/session";
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
||||||
import RecordScreen from '../screens/video-stack/record';
|
import RecordScreen from "../screens/video-stack/record";
|
||||||
|
|
||||||
// TODO: add ts support for assets folder to use imports
|
// TODO: add ts support for assets folder to use imports
|
||||||
const Icon = require('../assets/favicon.png')
|
const Icon = require("../assets/favicon.png");
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
const RecordStack = createNativeStackNavigator();
|
const RecordStack = createNativeStackNavigator();
|
||||||
|
|
||||||
// tabBarIcon configuration should live on separate file and contain all logic/icons/rendering for the Tabs
|
// tabBarIcon configuration should live on separate file and contain all logic/icons/rendering for the Tabs
|
||||||
const tabIcons = {
|
const tabIcons = {
|
||||||
'Session': <Image source={Icon} style={{ width: 20, height: 20 }} />,
|
Session: <Image source={Icon} style={{ width: 20, height: 20 }} />,
|
||||||
'VideoStack': <Image source={Icon} style={{ width: 20, height: 20 }} />,
|
VideoStack: <Image source={Icon} style={{ width: 20, height: 20 }} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
function VideoTabStack() {
|
function VideoTabStack() {
|
||||||
return (
|
return (
|
||||||
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
|
<RecordStack.Navigator screenOptions={{ headerShown: false }}>
|
||||||
<RecordStack.Screen
|
<RecordStack.Screen name="Record" component={RecordScreen} />
|
||||||
name="Record"
|
<RecordStack.Screen name="Camera" component={CameraScreen} />
|
||||||
component={RecordScreen}
|
</RecordStack.Navigator>
|
||||||
/>
|
);
|
||||||
<RecordStack.Screen
|
|
||||||
name="Camera"
|
|
||||||
component={CameraScreen}
|
|
||||||
/>
|
|
||||||
</RecordStack.Navigator>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,19 +35,27 @@ function VideoTabStack() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Tabs(): React.JSX.Element {
|
export default function Tabs(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
tabBarActiveTintColor: 'tomato',
|
tabBarActiveTintColor: "tomato",
|
||||||
tabBarInactiveTintColor: 'gray',
|
tabBarInactiveTintColor: "gray",
|
||||||
tabBarIcon: () => {
|
tabBarIcon: () => {
|
||||||
return tabIcons[route.name];
|
return tabIcons[route.name];
|
||||||
}
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tab.Screen name="Session" component={Session} options={{ tabBarLabel: 'Session' }} />
|
<Tab.Screen
|
||||||
<Tab.Screen name="VideoStack" component={VideoTabStack} options={{ tabBarLabel: 'Record' }} />
|
name="Session"
|
||||||
</Tab.Navigator>
|
component={Session}
|
||||||
);
|
options={{ tabBarLabel: "Session" }}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="VideoStack"
|
||||||
|
component={VideoTabStack}
|
||||||
|
options={{ tabBarLabel: "Record" }}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
|
import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
|
||||||
|
|
||||||
@ -15,105 +15,105 @@ import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
|
|||||||
// Currently working for Android builds, iOS has open issue #56
|
// Currently working for Android builds, iOS has open issue #56
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [phoneNumber, setPhoneNumber] = useState<string>("");
|
const [phoneNumber, setPhoneNumber] = useState<string>("");
|
||||||
const [code, setCode] = useState<string>("");
|
const [code, setCode] = useState<string>("");
|
||||||
|
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [confirm, setConfirm] =
|
const [confirm, setConfirm] =
|
||||||
useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
|
useState<FirebaseAuthTypes.ConfirmationResult | null>(null);
|
||||||
|
|
||||||
async function onAuthStateChanged(user: any) {
|
async function onAuthStateChanged(user: any) {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
if (user) {
|
if (user) {
|
||||||
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
||||||
const token = await auth().currentUser?.getIdToken();
|
const token = await auth().currentUser?.getIdToken();
|
||||||
// To debug/check token & user return, use these logs
|
// To debug/check token & user return, use these logs
|
||||||
// console.log(token) // token log
|
// console.log(token) // token log
|
||||||
// console.log(user) // user log
|
// console.log(user) // user log
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signInWithPhoneNumber(phoneNumber: string) {
|
async function signInWithPhoneNumber(phoneNumber: string) {
|
||||||
if (!phoneNumber) {
|
if (!phoneNumber) {
|
||||||
return Alert.alert(
|
return Alert.alert(
|
||||||
"Please enter a valid phone number with a country code",
|
"Please enter a valid phone number with a country code",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
|
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
|
||||||
setConfirm(confirmation);
|
setConfirm(confirmation);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO: implement more robust error handling by parsing err message
|
// TODO: implement more robust error handling by parsing err message
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"There was an error. Make sure you are using a country code (ex: +1 for US)",
|
"There was an error. Make sure you are using a country code (ex: +1 for US)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmCode() {
|
async function confirmCode() {
|
||||||
try {
|
try {
|
||||||
await confirm?.confirm(code);
|
await confirm?.confirm(code);
|
||||||
} catch {
|
} catch {
|
||||||
Alert.alert("Invalid code, please try again.");
|
Alert.alert("Invalid code, please try again.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
|
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
|
||||||
return subscriber;
|
return subscriber;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
|
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
|
||||||
<View style={{ alignItems: "center" }}>
|
<View style={{ alignItems: "center" }}>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{
|
style={{
|
||||||
width: "50%",
|
width: "50%",
|
||||||
height: 30,
|
height: 30,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "black",
|
borderColor: "black",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
}}
|
}}
|
||||||
placeholder="Phone"
|
placeholder="Phone"
|
||||||
textContentType="telephoneNumber"
|
textContentType="telephoneNumber"
|
||||||
keyboardType="phone-pad"
|
keyboardType="phone-pad"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChangeText={(value) => setPhoneNumber(value)}
|
onChangeText={(value) => setPhoneNumber(value)}
|
||||||
/>
|
/>
|
||||||
{confirm && (
|
{confirm && (
|
||||||
<TextInput
|
<TextInput
|
||||||
style={{
|
style={{
|
||||||
width: "50%",
|
width: "50%",
|
||||||
height: 30,
|
height: 30,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "black",
|
borderColor: "black",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
}}
|
}}
|
||||||
placeholder="Code"
|
placeholder="Code"
|
||||||
keyboardType="number-pad"
|
keyboardType="number-pad"
|
||||||
textContentType="oneTimeCode"
|
textContentType="oneTimeCode"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
value={code}
|
value={code}
|
||||||
onChangeText={(value) => setCode(value)}
|
onChangeText={(value) => setCode(value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
title={!confirm ? "Receive code" : "Confirm code"}
|
title={!confirm ? "Receive code" : "Confirm code"}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
!confirm ? signInWithPhoneNumber(phoneNumber) : confirmCode()
|
!confirm ? signInWithPhoneNumber(phoneNumber) : confirmCode()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
<Text style={{ marginTop: 10 }}>
|
<Text style={{ marginTop: 10 }}>
|
||||||
Display name: {user?.displayName}
|
Display name: {user?.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>Phone number: {user?.phoneNumber}</Text>
|
<Text>Phone number: {user?.phoneNumber}</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import { View, StyleSheet } from "react-native";
|
import { View, StyleSheet } from "react-native";
|
||||||
import BarGraph from '../component/charts/bar-graph/bar-graph';
|
import BarGraph from "../component/charts/bar-graph/bar-graph";
|
||||||
import { graph_data_two_measures } from '../mock/charts/mock-data';
|
import { graph_data_two_measures } from "../mock/charts/mock-data";
|
||||||
|
|
||||||
// Session Mock - can be used for session summary screen using a query handler component
|
// Session Mock - can be used for session summary screen using a query handler component
|
||||||
// BarGraph component using mocked data currently
|
// BarGraph component using mocked data currently
|
||||||
export default function SessionScreen() {
|
export default function SessionScreen() {
|
||||||
return (
|
return (
|
||||||
|
<View style={StyleSheet.absoluteFill}>
|
||||||
<View style={StyleSheet.absoluteFill}>
|
<BarGraph data={graph_data_two_measures} />
|
||||||
<BarGraph data={graph_data_two_measures} />
|
</View>
|
||||||
</View>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
@ -1,145 +1,150 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from "react";
|
||||||
import { View, TextInput, TouchableWithoutFeedback, Text, TouchableOpacity, Keyboard } from 'react-native';
|
import {
|
||||||
import DropDownPicker from 'react-native-dropdown-picker';
|
View,
|
||||||
import { recordStyles as styles } from './styles';
|
TextInput,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
Keyboard,
|
||||||
|
} from "react-native";
|
||||||
|
import DropDownPicker from "react-native-dropdown-picker";
|
||||||
|
import { recordStyles as styles } from "./styles";
|
||||||
|
|
||||||
interface CameraScreenParams {
|
interface CameraScreenParams {
|
||||||
gameType: string;
|
gameType: string;
|
||||||
tableSize: string;
|
tableSize: string;
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
location: string;
|
location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record Screen
|
// Record Screen
|
||||||
// Precedes Camera.tsx
|
// Precedes Camera.tsx
|
||||||
// Can be made into Modal when ready
|
// Can be made into Modal when ready
|
||||||
export default function RecordScreen({ navigation }): React.JSX.Element {
|
export default function RecordScreen({ navigation }): React.JSX.Element {
|
||||||
|
// Game type dropdown
|
||||||
|
const [gameTypeOpen, setGameTypeOpen] = useState<boolean>(false);
|
||||||
|
const [gameType, setGameType] = useState<string | null>(null); // This is a dropdown
|
||||||
|
const [gameTypes, setGameTypes] = useState([
|
||||||
|
{ label: "Free Play", value: "freePlay" },
|
||||||
|
{ label: "Straight Pool", value: "straightPool" },
|
||||||
|
{ label: "Nine Ball", value: "nineBall" },
|
||||||
|
]);
|
||||||
|
const onGameTypeOpen = useCallback(() => {
|
||||||
|
setTableSizeOpen(false);
|
||||||
|
setTagsOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Game type dropdown
|
// Table size dropdown
|
||||||
const [gameTypeOpen, setGameTypeOpen] = useState<boolean>(false)
|
const [tableSizeOpen, setTableSizeOpen] = useState<boolean>(false);
|
||||||
const [gameType, setGameType] = useState<string | null>(null) // This is a dropdown
|
const [tableSize, setTableSize] = useState<string>("");
|
||||||
const [gameTypes, setGameTypes] = useState([
|
const [tableSizes, setTableSizes] = useState([
|
||||||
{ label: 'Free Play', value: 'freePlay' },
|
{ label: `9'`, value: "nineFoot" },
|
||||||
{ label: 'Straight Pool', value: 'straightPool' },
|
{ label: `8'`, value: "eightFoot" },
|
||||||
{ label: 'Nine Ball', value: 'nineBall' }
|
{ label: "7", value: "sevenFoot" },
|
||||||
]);
|
]);
|
||||||
const onGameTypeOpen = useCallback(() => {
|
const onTableSizeOpen = useCallback(() => {
|
||||||
setTableSizeOpen(false);
|
setGameTypeOpen(false);
|
||||||
setTagsOpen(false);
|
setTagsOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Table size dropdown
|
// Tags multi-select dropdown
|
||||||
const [tableSizeOpen, setTableSizeOpen] = useState<boolean>(false)
|
const [tagsOpen, setTagsOpen] = useState<boolean>(false);
|
||||||
const [tableSize, setTableSize] = useState<string>('')
|
const [tags, setTags] = useState<Array<string>>([]);
|
||||||
const [tableSizes, setTableSizes] = useState([
|
const [tagsList, setTagsList] = useState([
|
||||||
{ label: `9'`, value: 'nineFoot' },
|
{ label: `Tag1`, value: "tag1" },
|
||||||
{ label: `8'`, value: 'eightFoot' },
|
{ label: `Tag2`, value: "tag2" },
|
||||||
{ label: '7', value: 'sevenFoot' }
|
{ label: "Tag3", value: "tag3" },
|
||||||
]);
|
]);
|
||||||
const onTableSizeOpen = useCallback(() => {
|
const onTagsOpen = useCallback(() => {
|
||||||
setGameTypeOpen(false);
|
setTableSizeOpen(false);
|
||||||
setTagsOpen(false);
|
setGameTypeOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Tags multi-select dropdown
|
// Location
|
||||||
const [tagsOpen, setTagsOpen] = useState<boolean>(false)
|
const [location, setLocation] = useState<string>("");
|
||||||
const [tags, setTags] = useState<Array<string>>([])
|
|
||||||
const [tagsList, setTagsList] = useState([
|
|
||||||
{ label: `Tag1`, value: 'tag1' },
|
|
||||||
{ label: `Tag2`, value: 'tag2' },
|
|
||||||
{ label: 'Tag3', value: 'tag3' }
|
|
||||||
]);
|
|
||||||
const onTagsOpen = useCallback(() => {
|
|
||||||
setTableSizeOpen(false);
|
|
||||||
setGameTypeOpen(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Location
|
const handleSubmit = () => {
|
||||||
const [location, setLocation] = useState<string>('')
|
// needs to pass info as params or store in a context/state provider
|
||||||
|
const params: CameraScreenParams = {
|
||||||
|
gameType: gameType,
|
||||||
|
tableSize: tableSize,
|
||||||
|
tags: tags,
|
||||||
|
location: location,
|
||||||
|
};
|
||||||
|
navigation.push("Camera", params);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const dropDownStyles = {
|
||||||
// needs to pass info as params or store in a context/state provider
|
style: styles.dropdownStyle,
|
||||||
const params: CameraScreenParams = {
|
};
|
||||||
gameType: gameType,
|
|
||||||
tableSize: tableSize,
|
|
||||||
tags: tags,
|
|
||||||
location: location
|
|
||||||
};
|
|
||||||
navigation.push('Camera', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropDownStyles = {
|
return (
|
||||||
style: styles.dropdownStyle,
|
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
|
||||||
};
|
<View style={styles.container}>
|
||||||
|
<View style={styles.dropdownContainer}>
|
||||||
|
<Text style={styles.dropdownTitle}>Game Type</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
zIndex={3000}
|
||||||
|
zIndexInverse={1000}
|
||||||
|
open={gameTypeOpen}
|
||||||
|
value={gameType}
|
||||||
|
items={gameTypes}
|
||||||
|
setOpen={setGameTypeOpen}
|
||||||
|
setValue={setGameType}
|
||||||
|
setItems={setGameTypes}
|
||||||
|
onOpen={onGameTypeOpen}
|
||||||
|
{...dropDownStyles}
|
||||||
|
/>
|
||||||
|
<Text style={styles.dropdownTitle}>Table size</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
zIndex={2000}
|
||||||
|
zIndexInverse={2000}
|
||||||
|
open={tableSizeOpen}
|
||||||
|
value={tableSize}
|
||||||
|
items={tableSizes}
|
||||||
|
setOpen={setTableSizeOpen}
|
||||||
|
setValue={setTableSize}
|
||||||
|
setItems={setTableSizes}
|
||||||
|
onOpen={onTableSizeOpen}
|
||||||
|
{...dropDownStyles}
|
||||||
|
/>
|
||||||
|
<Text style={styles.dropdownTitle}>Tags</Text>
|
||||||
|
<DropDownPicker
|
||||||
|
zIndex={1000}
|
||||||
|
zIndexInverse={3000}
|
||||||
|
multiple
|
||||||
|
min={0}
|
||||||
|
max={5}
|
||||||
|
open={tagsOpen}
|
||||||
|
value={tags}
|
||||||
|
items={tagsList}
|
||||||
|
setOpen={setTagsOpen}
|
||||||
|
setValue={setTags}
|
||||||
|
setItems={setTagsList}
|
||||||
|
onOpen={onTagsOpen}
|
||||||
|
{...dropDownStyles}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.dropdownTitle}>Location</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Location"
|
||||||
|
value={location}
|
||||||
|
onChangeText={(value) => setLocation(value)}
|
||||||
|
/>
|
||||||
|
|
||||||
return (
|
<View style={styles.buttonContainer}>
|
||||||
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
|
<TouchableOpacity
|
||||||
<View style={styles.container}>
|
style={styles.buttonStyle}
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
<View style={styles.dropdownContainer}>
|
>
|
||||||
<Text style={styles.dropdownTitle}>Game Type</Text>
|
<Text style={styles.buttonText}>Back</Text>
|
||||||
<DropDownPicker
|
</TouchableOpacity>
|
||||||
zIndex={3000}
|
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
|
||||||
zIndexInverse={1000}
|
<Text style={styles.buttonText}>Next</Text>
|
||||||
open={gameTypeOpen}
|
</TouchableOpacity>
|
||||||
value={gameType}
|
</View>
|
||||||
items={gameTypes}
|
</View>
|
||||||
setOpen={setGameTypeOpen}
|
</TouchableWithoutFeedback>
|
||||||
setValue={setGameType}
|
);
|
||||||
setItems={setGameTypes}
|
|
||||||
onOpen={onGameTypeOpen}
|
|
||||||
{...dropDownStyles}
|
|
||||||
/>
|
|
||||||
<Text style={styles.dropdownTitle}>Table size</Text>
|
|
||||||
<DropDownPicker
|
|
||||||
zIndex={2000}
|
|
||||||
zIndexInverse={2000}
|
|
||||||
open={tableSizeOpen}
|
|
||||||
value={tableSize}
|
|
||||||
items={tableSizes}
|
|
||||||
setOpen={setTableSizeOpen}
|
|
||||||
setValue={setTableSize}
|
|
||||||
setItems={setTableSizes}
|
|
||||||
onOpen={onTableSizeOpen}
|
|
||||||
{...dropDownStyles}
|
|
||||||
|
|
||||||
/>
|
|
||||||
<Text style={styles.dropdownTitle}>Tags</Text>
|
|
||||||
<DropDownPicker
|
|
||||||
zIndex={1000}
|
|
||||||
zIndexInverse={3000}
|
|
||||||
multiple
|
|
||||||
min={0}
|
|
||||||
max={5}
|
|
||||||
open={tagsOpen}
|
|
||||||
value={tags}
|
|
||||||
items={tagsList}
|
|
||||||
setOpen={setTagsOpen}
|
|
||||||
setValue={setTags}
|
|
||||||
setItems={setTagsList}
|
|
||||||
onOpen={onTagsOpen}
|
|
||||||
{...dropDownStyles}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Text style={styles.dropdownTitle}>Location</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
placeholder='Location'
|
|
||||||
value={location}
|
|
||||||
onChangeText={(value) => setLocation(value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View style={styles.buttonContainer}>
|
|
||||||
<TouchableOpacity style={styles.buttonStyle} onPress={() => navigation.goBack()}>
|
|
||||||
<Text style={styles.buttonText}>Back</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity style={styles.buttonStyle} onPress={handleSubmit}>
|
|
||||||
<Text style={styles.buttonText}>Next</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,57 +1,57 @@
|
|||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
export const recordStyles = StyleSheet.create({
|
export const recordStyles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center',
|
justifyContent: "center",
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
dropdownContainer: {
|
dropdownContainer: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
zIndex: 50
|
zIndex: 50,
|
||||||
},
|
},
|
||||||
dropdownTitle: {
|
dropdownTitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '500',
|
fontWeight: "500",
|
||||||
marginBottom: 5,
|
marginBottom: 5,
|
||||||
alignSelf: 'flex-start'
|
alignSelf: "flex-start",
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'grey',
|
borderColor: "grey",
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
},
|
},
|
||||||
buttonContainer: {
|
buttonContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
justifyContent: 'space-between',
|
justifyContent: "space-between",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
},
|
},
|
||||||
buttonStyle: {
|
buttonStyle: {
|
||||||
backgroundColor: 'lightblue',
|
backgroundColor: "lightblue",
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
margin: 10,
|
margin: 10,
|
||||||
},
|
},
|
||||||
buttonText: {
|
buttonText: {
|
||||||
color: 'white',
|
color: "white",
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
dropdownStyle: {
|
dropdownStyle: {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: "#ffffff",
|
||||||
borderColor: '#D1D1D1',
|
borderColor: "#D1D1D1",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
dropdownContainerStyle: {
|
dropdownContainerStyle: {
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
borderColor: '#D1D1D1',
|
borderColor: "#D1D1D1",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
36
styles.ts
36
styles.ts
@ -2,25 +2,25 @@
|
|||||||
// 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",
|
||||||
themeBrown: '#D9AA84',
|
themeBrown: "#D9AA84",
|
||||||
panelWhite: '#F2FBFE',
|
panelWhite: "#F2FBFE",
|
||||||
tournamentBlue: '#50a6c2',
|
tournamentBlue: "#50a6c2",
|
||||||
blueCloth: '#539dc2',
|
blueCloth: "#539dc2",
|
||||||
buttonBlue: '#1987ff',
|
buttonBlue: "#1987ff",
|
||||||
textWhite: '#ffffff'
|
textWhite: "#ffffff",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shadows = {
|
export const shadows = {
|
||||||
standard: {
|
standard: {
|
||||||
shadowColor: '#000000',
|
shadowColor: "#000000",
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 3
|
height: 3,
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 4.65,
|
shadowRadius: 4.65,
|
||||||
elevation: 3
|
elevation: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from "@testing-library/react-native";
|
||||||
import BarGraph from '../../component/charts/bar-graph/bar-graph';
|
import BarGraph from "../../component/charts/bar-graph/bar-graph";
|
||||||
import { graph_data_two_measures } from '../../mock/charts/mock-data';
|
import { graph_data_two_measures } from "../../mock/charts/mock-data";
|
||||||
|
|
||||||
describe('BarGraph Component Tests', () => {
|
describe("BarGraph Component Tests", () => {
|
||||||
|
it("renders correctly with data", () => {
|
||||||
it('renders correctly with data', () => {
|
const { getByTestId } = render(
|
||||||
const { getByTestId } = render(<BarGraph data={graph_data_two_measures} testID='1'/>);
|
<BarGraph data={graph_data_two_measures} testID="1" />,
|
||||||
expect(getByTestId(`bar-graph-1`)).toBeTruthy();
|
);
|
||||||
});
|
expect(getByTestId(`bar-graph-1`)).toBeTruthy();
|
||||||
|
});
|
||||||
it('does not render without data', () => {
|
|
||||||
// Have to ts-ignore to test null data conditions
|
|
||||||
// @ts-ignore
|
|
||||||
const { queryByTestId } = render(<BarGraph testID='2'/>);
|
|
||||||
expect(queryByTestId(`bar-graph-2`)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
it("does not render without data", () => {
|
||||||
|
// Have to ts-ignore to test null data conditions
|
||||||
|
// @ts-ignore
|
||||||
|
const { queryByTestId } = render(<BarGraph testID="2" />);
|
||||||
|
expect(queryByTestId(`bar-graph-2`)).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,38 +1,42 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from "@testing-library/react-native";
|
||||||
import "@testing-library/jest-native/extend-expect";
|
import "@testing-library/jest-native/extend-expect";
|
||||||
|
|
||||||
import ChartLabel from '../../component/charts/chart-label/chart-label';
|
import ChartLabel from "../../component/charts/chart-label/chart-label";
|
||||||
|
|
||||||
describe('ChartLabel Component Tests', () => {
|
describe("ChartLabel Component Tests", () => {
|
||||||
const mockData = {
|
const mockData = {
|
||||||
yLabels: [
|
yLabels: [
|
||||||
{ displayName: 'Shots Taken', axis: 'LEFT' as 'LEFT', color: '#598EBB' },
|
{ displayName: "Shots Taken", axis: "LEFT" as "LEFT", color: "#598EBB" },
|
||||||
{ displayName:'Make Percentage', axis: 'RIGHT' as 'RIGHT', color: '#F2D4BC'}
|
{
|
||||||
],
|
displayName: "Make Percentage",
|
||||||
title: 'Shots Taken / Make Percentage by Cut Angle'
|
axis: "RIGHT" as "RIGHT",
|
||||||
};
|
color: "#F2D4BC",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
title: "Shots Taken / Make Percentage by Cut Angle",
|
||||||
|
};
|
||||||
|
|
||||||
it('should render the correct labels given yLabels', () => {
|
it("should render the correct labels given yLabels", () => {
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />
|
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockData.yLabels.forEach(label => {
|
mockData.yLabels.forEach((label) => {
|
||||||
expect(getByText(label.displayName)).toBeTruthy();
|
expect(getByText(label.displayName)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the correct number of label boxes', () => {
|
it("should render the correct number of label boxes", () => {
|
||||||
const { getAllByText } = render(
|
const { getAllByText } = render(
|
||||||
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />
|
<ChartLabel title={mockData.title} yLabels={mockData.yLabels} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Assuming displayName is unique and used only for labels
|
// Assuming displayName is unique and used only for labels
|
||||||
const labelElements = mockData.yLabels.map(label =>
|
const labelElements = mockData.yLabels
|
||||||
getAllByText(label.displayName)
|
.map((label) => getAllByText(label.displayName))
|
||||||
).flat();
|
.flat();
|
||||||
|
|
||||||
expect(labelElements.length).toBe(mockData.yLabels.length);
|
expect(labelElements.length).toBe(mockData.yLabels.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,31 +1,31 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Text } from 'react-native'
|
import { Text } from "react-native";
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from "@testing-library/react-native";
|
||||||
import "@testing-library/jest-native/extend-expect";
|
import "@testing-library/jest-native/extend-expect";
|
||||||
|
|
||||||
import ChartView from '../../component/charts/chart-view';
|
import ChartView from "../../component/charts/chart-view";
|
||||||
|
|
||||||
describe('ChartView Component Tests', () => {
|
describe("ChartView Component Tests", () => {
|
||||||
it('applies the passed style prop correctly', () => {
|
it("applies the passed style prop correctly", () => {
|
||||||
const testStyle = { backgroundColor: 'blue', padding: 10 };
|
const testStyle = { backgroundColor: "blue", padding: 10 };
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<ChartView style={testStyle} testID="chart-view">
|
<ChartView style={testStyle} testID="chart-view">
|
||||||
<Text>Test Child</Text>
|
<Text>Test Child</Text>
|
||||||
</ChartView>
|
</ChartView>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartView = getByTestId('chart-view');
|
const chartView = getByTestId("chart-view");
|
||||||
expect(chartView.props.style).toEqual(expect.arrayContaining([testStyle]));
|
expect(chartView.props.style).toEqual(expect.arrayContaining([testStyle]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders children correctly', () => {
|
it("renders children correctly", () => {
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<ChartView>
|
<ChartView>
|
||||||
<Text testID="child-text">Child Component</Text>
|
<Text testID="child-text">Child Component</Text>
|
||||||
</ChartView>
|
</ChartView>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const childText = getByText('Child Component');
|
const childText = getByText("Child Component");
|
||||||
expect(childText).toBeTruthy();
|
expect(childText).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,89 +1,97 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from "@testing-library/react-native";
|
||||||
import "@testing-library/jest-native/extend-expect";
|
import "@testing-library/jest-native/extend-expect";
|
||||||
import * as scale from 'd3-scale'
|
import * as scale from "d3-scale";
|
||||||
|
|
||||||
import { CustomBars } from '../../component/charts/custom-bars';
|
|
||||||
import { calculateBarOrigin, drawBarPath } from '../../component/charts/custom-bar-utils';
|
|
||||||
|
|
||||||
|
import { CustomBars } from "../../component/charts/custom-bars";
|
||||||
|
import {
|
||||||
|
calculateBarOrigin,
|
||||||
|
drawBarPath,
|
||||||
|
} from "../../component/charts/custom-bar-utils";
|
||||||
|
|
||||||
const mockYScaleFunction = scale.scaleLinear();
|
const mockYScaleFunction = scale.scaleLinear();
|
||||||
const mockXScaleFunction = scale.scaleBand();
|
const mockXScaleFunction = scale.scaleBand();
|
||||||
const mockBandwidth = 100;
|
const mockBandwidth = 100;
|
||||||
const mockCombinedData = [
|
const mockCombinedData = [
|
||||||
{
|
{
|
||||||
data: [{ value: 10 }, { value: 20 }],
|
data: [{ value: 10 }, { value: 20 }],
|
||||||
svg: { fill: 'red' },
|
svg: { fill: "red" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const mockRawData = [
|
const mockRawData = [
|
||||||
{ label: 'Full ball', shotsTaken: 10, makePercentage: 20 },
|
{ label: "Full ball", shotsTaken: 10, makePercentage: 20 },
|
||||||
];
|
];
|
||||||
const mockBarColors = ['red', 'blue'];
|
const mockBarColors = ["red", "blue"];
|
||||||
const mockGap = 2;
|
const mockGap = 2;
|
||||||
const mockRoundedRadius = 4;
|
const mockRoundedRadius = 4;
|
||||||
describe('CustomBars Component Tests', () => {
|
describe("CustomBars Component Tests", () => {
|
||||||
it('should render correct number of Svg and Bar components', () => {
|
it("should render correct number of Svg and Bar components", () => {
|
||||||
const { getAllByTestId } = render(
|
const { getAllByTestId } = render(
|
||||||
<CustomBars
|
<CustomBars
|
||||||
x={mockXScaleFunction}
|
x={mockXScaleFunction}
|
||||||
y={mockYScaleFunction}
|
y={mockYScaleFunction}
|
||||||
bandwidth={mockBandwidth}
|
bandwidth={mockBandwidth}
|
||||||
barData={mockCombinedData}
|
barData={mockCombinedData}
|
||||||
xValues={mockRawData}
|
xValues={mockRawData}
|
||||||
barColors={mockBarColors}
|
barColors={mockBarColors}
|
||||||
gap={mockGap}
|
gap={mockGap}
|
||||||
roundedRadius={mockRoundedRadius}
|
roundedRadius={mockRoundedRadius}
|
||||||
/>
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const svgs = getAllByTestId(/svg-/);
|
const svgs = getAllByTestId(/svg-/);
|
||||||
const bars = getAllByTestId(/bar-/);
|
const bars = getAllByTestId(/bar-/);
|
||||||
|
|
||||||
expect(svgs.length).toBe(mockRawData.length);
|
|
||||||
expect(bars.length).toBe(mockCombinedData.length * mockRawData.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
expect(svgs.length).toBe(mockRawData.length);
|
||||||
|
expect(bars.length).toBe(mockCombinedData.length * mockRawData.length);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Bar utility functions', () => {
|
describe("Bar utility functions", () => {
|
||||||
describe('calculateBarOrigin', () => {
|
describe("calculateBarOrigin", () => {
|
||||||
it('calculates properties correctly', () => {
|
it("calculates properties correctly", () => {
|
||||||
const mockData = { value: 10 };
|
const mockData = { value: 10 };
|
||||||
const mockIndex = 1;
|
const mockIndex = 1;
|
||||||
const mockBarNumber = 2;
|
const mockBarNumber = 2;
|
||||||
const mockBarWidth = 20;
|
const mockBarWidth = 20;
|
||||||
const mockGap = 5;
|
const mockGap = 5;
|
||||||
|
|
||||||
const result = calculateBarOrigin({
|
const result = calculateBarOrigin({
|
||||||
scaleX: mockXScaleFunction,
|
scaleX: mockXScaleFunction,
|
||||||
scaleY: mockYScaleFunction,
|
scaleY: mockYScaleFunction,
|
||||||
data: mockData,
|
data: mockData,
|
||||||
index: mockIndex,
|
index: mockIndex,
|
||||||
barNumber: mockBarNumber,
|
barNumber: mockBarNumber,
|
||||||
barWidth: mockBarWidth,
|
barWidth: mockBarWidth,
|
||||||
gap: mockGap
|
gap: mockGap,
|
||||||
});
|
});
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
xOrigin: expect.any(Number),
|
xOrigin: expect.any(Number),
|
||||||
yOrigin: expect.any(Number),
|
yOrigin: expect.any(Number),
|
||||||
height: expect.any(Number)
|
height: expect.any(Number),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('drawBarPath', () => {
|
describe("drawBarPath", () => {
|
||||||
it('generates a correct SVG path string', () => {
|
it("generates a correct SVG path string", () => {
|
||||||
const xOrigin = 50;
|
const xOrigin = 50;
|
||||||
const yOrigin = 100;
|
const yOrigin = 100;
|
||||||
const barWidth = 20;
|
const barWidth = 20;
|
||||||
const height = 60;
|
const height = 60;
|
||||||
const roundedRadius = 10;
|
const roundedRadius = 10;
|
||||||
|
|
||||||
const path = drawBarPath(xOrigin, yOrigin, barWidth, height, roundedRadius);
|
const path = drawBarPath(
|
||||||
const expectedPath = 'M50,160L50,110A10,10,0,0,1,60,100L60,100A10,10,0,0,1,70,110L70,160L50,160Z'
|
xOrigin,
|
||||||
|
yOrigin,
|
||||||
|
barWidth,
|
||||||
|
height,
|
||||||
|
roundedRadius,
|
||||||
|
);
|
||||||
|
const expectedPath =
|
||||||
|
"M50,160L50,110A10,10,0,0,1,60,100L60,100A10,10,0,0,1,70,110L70,160L50,160Z";
|
||||||
|
|
||||||
expect(path).toBe(expectedPath);
|
expect(path).toBe(expectedPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -1,21 +1,20 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { render } from '@testing-library/react-native';
|
import { render } from "@testing-library/react-native";
|
||||||
import LineGraph from '../../component/charts/line-graph/line-graph';
|
import LineGraph from "../../component/charts/line-graph/line-graph";
|
||||||
import { line_chart_two_y_data } from '../../mock/charts/mock-data';
|
import { line_chart_two_y_data } from "../../mock/charts/mock-data";
|
||||||
|
|
||||||
describe('LineGraph Component Tests', () => {
|
describe("LineGraph Component Tests", () => {
|
||||||
|
it("renders correctly with data", () => {
|
||||||
it('renders correctly with data', () => {
|
const { getByTestId } = render(
|
||||||
const { getByTestId } = render(<LineGraph data={line_chart_two_y_data} testID='1'/>);
|
<LineGraph data={line_chart_two_y_data} testID="1" />,
|
||||||
expect(getByTestId(`line-graph-1`)).toBeTruthy();
|
);
|
||||||
|
expect(getByTestId(`line-graph-1`)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render without data', () => {
|
|
||||||
// Have to ts-ignore to test null data conditions
|
|
||||||
// @ts-ignore
|
|
||||||
const { queryByTestId } = render(<LineGraph testID='2'/>);
|
|
||||||
expect(queryByTestId(`line-graph-2`)).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
it("does not render without data", () => {
|
||||||
|
// Have to ts-ignore to test null data conditions
|
||||||
|
// @ts-ignore
|
||||||
|
const { queryByTestId } = render(<LineGraph testID="2" />);
|
||||||
|
expect(queryByTestId(`line-graph-2`)).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,58 +1,66 @@
|
|||||||
import { renderHook } from '@testing-library/react-native';
|
import { renderHook } from "@testing-library/react-native";
|
||||||
import { useGraphData } from '../../component/charts/use-graph-data';
|
import { useGraphData } from "../../component/charts/use-graph-data";
|
||||||
import { GraphData, GraphProps } from '../../component/charts/graph-types';
|
import { GraphData, GraphProps } from "../../component/charts/graph-types";
|
||||||
|
|
||||||
describe('useGraphData', () => {
|
describe("useGraphData", () => {
|
||||||
it('should return correctly processed data from convertToGraphData', () => {
|
it("should return correctly processed data from convertToGraphData", () => {
|
||||||
// mock values
|
// mock values
|
||||||
const mockGraphData: GraphData = {
|
const mockGraphData: GraphData = {
|
||||||
xValues: ['full hit', '3/4 ball ball', '1/2 ball'],
|
xValues: ["full hit", "3/4 ball ball", "1/2 ball"],
|
||||||
yValues: [
|
yValues: [
|
||||||
{ key: 'left', values: [10, 20, 30] },
|
{ key: "left", values: [10, 20, 30] },
|
||||||
{ key: 'right', values: [40, 50, 60] }
|
{ key: "right", values: [40, 50, 60] },
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockProps: Partial<GraphProps> = {
|
const mockProps: Partial<GraphProps> = {
|
||||||
yAxisProps: {
|
yAxisProps: {
|
||||||
maxLeftYAxisValue: 30,
|
maxLeftYAxisValue: 30,
|
||||||
maxRightYAxisValue: 60,
|
maxRightYAxisValue: 60,
|
||||||
selectedLeftYAxisLabel: 'left',
|
selectedLeftYAxisLabel: "left",
|
||||||
selectedRightYAxisLabel: 'right',
|
selectedRightYAxisLabel: "right",
|
||||||
formatRightYAxisLabel: (value) => `${value}%`,
|
formatRightYAxisLabel: (value) => `${value}%`,
|
||||||
formatLeftYAxisLabel: (value) => `${value}%`
|
formatLeftYAxisLabel: (value) => `${value}%`,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useGraphData(mockGraphData, mockProps));
|
||||||
|
// values expected
|
||||||
|
const expectedYData = [
|
||||||
|
{
|
||||||
|
data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }],
|
||||||
|
svg: { fill: "transparent" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: [{ value: 66.67 }, { value: 83.33 }, { value: 100 }],
|
||||||
|
svg: { fill: "transparent" },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const expectedLeftLabels = { key: "left", values: [10, 20, 30] };
|
||||||
|
const expectedRightLabels = { key: "right", values: [40, 50, 60] };
|
||||||
|
|
||||||
const { result } = renderHook(() => useGraphData(mockGraphData, mockProps));
|
expect(result.current).toBeDefined();
|
||||||
// values expected
|
expect(result.current.xValues).toEqual([
|
||||||
const expectedYData = [
|
"full hit",
|
||||||
{
|
"3/4 ball ball",
|
||||||
data: [{ value: 33.33 }, { value: 66.67 }, { value: 100 }],
|
"1/2 ball",
|
||||||
svg: { fill: 'transparent' },
|
]);
|
||||||
},
|
result.current.yData.forEach((yDataItem, index) => {
|
||||||
{
|
yDataItem.data.forEach((dataItem, dataIndex) => {
|
||||||
data: [{ value: 66.67 }, { value: 83.33 }, { value: 100 }],
|
expect(dataItem.value).toBeCloseTo(
|
||||||
svg: { fill: 'transparent' },
|
expectedYData[index].data[dataIndex].value,
|
||||||
},
|
2,
|
||||||
];
|
);
|
||||||
const expectedLeftLabels = { key: 'left', values: [10, 20, 30] };
|
});
|
||||||
const expectedRightLabels = { key: 'right', values: [40, 50, 60] };
|
});
|
||||||
|
expect(result.current.yAxisLeftLabels).toEqual(expectedLeftLabels);
|
||||||
|
expect(result.current.yAxisRightLabels).toEqual(expectedRightLabels);
|
||||||
expect(result.current).toBeDefined();
|
expect(
|
||||||
expect(result.current.xValues).toEqual(['full hit', '3/4 ball ball', '1/2 ball']);
|
result.current.defaultProps.yAxisProps.selectedLeftYAxisLabel,
|
||||||
result.current.yData.forEach((yDataItem, index) => {
|
).toEqual("left");
|
||||||
yDataItem.data.forEach((dataItem, dataIndex) => {
|
expect(
|
||||||
expect(dataItem.value).toBeCloseTo(expectedYData[index].data[dataIndex].value, 2);
|
result.current.defaultProps.yAxisProps.selectedRightYAxisLabel,
|
||||||
});
|
).toEqual("right");
|
||||||
});
|
expect(result.current.defaultProps).toBeDefined();
|
||||||
expect(result.current.yAxisLeftLabels).toEqual(expectedLeftLabels);
|
});
|
||||||
expect(result.current.yAxisRightLabels).toEqual(expectedRightLabels);
|
|
||||||
expect(result.current.defaultProps.yAxisProps.selectedLeftYAxisLabel).toEqual('left');
|
|
||||||
expect(result.current.defaultProps.yAxisProps.selectedRightYAxisLabel).toEqual('right');
|
|
||||||
expect(result.current.defaultProps).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["node_modules", "./react-native-vision-camera/package"],
|
"exclude": ["node_modules", "./react-native-vision-camera/package"],
|
||||||
"extends": ["expo/tsconfig.base"]
|
"extends": ["expo/tsconfig.base"]
|
||||||
}
|
}
|
||||||
|
@ -9513,6 +9513,11 @@ prelude-ls@^1.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||||
|
|
||||||
|
prettier-plugin-organize-imports@^3.2.4:
|
||||||
|
version "3.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e"
|
||||||
|
integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==
|
||||||
|
|
||||||
pretty-bytes@5.6.0:
|
pretty-bytes@5.6.0:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||||
|
Loading…
Reference in New Issue
Block a user