Actually respect prettier configuration

This commit is contained in:
Ivan Malison 2024-02-03 20:23:31 -07:00
parent a6883a624a
commit 2276605e6d
37 changed files with 1629 additions and 1497 deletions

View File

@ -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
View File

@ -0,0 +1,4 @@
/android
/ios
/react-native-vision-camera
flake.lock

View File

@ -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"
} }
} }
} }

View File

@ -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;

View File

@ -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}`}
/> />
); );
}; };

View File

@ -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> );
);
} }

View File

@ -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 },
}); });

View File

@ -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;

View File

@ -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;

View File

@ -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>
)); ));
}; };

View File

@ -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>
);
}; };

View File

@ -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,
}; };

View File

@ -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;
} }

View File

@ -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 };
}; };

View File

@ -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;

View File

@ -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,
}; };
}; };

View File

@ -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" }],
}, },
}); });

View File

@ -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;

View File

@ -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;
} };

View File

@ -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"
} }

View File

@ -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,

View File

@ -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],
} },
] ],
}; };

View File

@ -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>
)
} }

View File

@ -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>
);
} }

View File

@ -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>
); );
} }

View File

@ -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> );
)
} }

View File

@ -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>
)
} }

View File

@ -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,
}, },
}); });

View File

@ -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,
}, },
}; };

View File

@ -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();
});
}); });

View File

@ -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);
}); });
}); });

View File

@ -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();
}); });
}); });

View File

@ -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);
}); });
}); });
}); });

View File

@ -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();
});
}); });

View File

@ -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();
});
}); });

View File

@ -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"]
} }

View File

@ -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"