Skip to content

Commit 09f96e1

Browse files
committed
history + result
1 parent 7191183 commit 09f96e1

29 files changed

+536
-94
lines changed

src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {NativeRouter, Route, Routes, useNavigate} from 'react-router-native';
33
import {useBackHandler} from '@react-native-community/hooks';
44
import styled from '@emotion/native';
55

6-
import {History, Menu, Race, Result, Start} from './screens';
6+
import {History, HistoryList, Menu, Race, Result, Start} from './screens';
77
import {StoreProvider} from './store/StoreProvider';
88
import {AppThemeProvider} from './themes/ThemeProvider';
99

@@ -34,7 +34,8 @@ const App = () => (
3434
<Route path="/menu" element={<Menu />} />
3535
<Route path="/race" element={<Race />} />
3636
<Route path="/result" element={<Result />} />
37-
<Route path="/history" element={<History />} />
37+
<Route path="/history" element={<HistoryList />} />
38+
<Route path="/history/:date" element={<History />} />
3839
</Routes>
3940
</BackPressHandler>
4041
</NativeRouter>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React, {FC} from 'react';
2+
import styled from '@emotion/native';
3+
4+
import {BestLap, Laps} from './components';
5+
import {Store} from '../../store/Store.types';
6+
7+
const Content = styled.View`
8+
background: ${props => props.theme.bgColor};
9+
width: 100%;
10+
flex-grow: 1;
11+
flex-direction: column;
12+
`;
13+
14+
interface Props extends Pick<Store, 'laps' | 'startPoint'> {}
15+
16+
export const RaceResult: FC<Props> = ({laps, startPoint}) => (
17+
<Content>
18+
<BestLap laps={laps} />
19+
<Laps laps={laps} startPoint={startPoint} />
20+
</Content>
21+
);

src/screens/Result/components/BestLap.tsx renamed to src/components/RaceResult/components/BestLap.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import styled from '@emotion/native';
55
import {StoreContext} from '../../../contexts';
66
import {getBestLap} from '../../../utils/race';
77
import {timeFormatter} from '../../../utils/formatters';
8+
import {Lap, Store} from '../../../store/Store.types';
89

910
const Title = styled.Text`
1011
font-weight: 400;
@@ -18,10 +19,12 @@ const Time = styled.Text`
1819
color: ${props => props.theme.positiveColor};
1920
`;
2021

21-
export const BestLap: FC = () => {
22-
const {state} = useContext(StoreContext);
22+
interface Props {
23+
laps: Store['laps'];
24+
}
2325

24-
const bestLap = useMemo(() => getBestLap(state.laps), [state.laps]);
26+
export const BestLap: FC<Props> = ({laps}) => {
27+
const bestLap = useMemo(() => getBestLap(laps), [laps]);
2528

2629
return (
2730
<View>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React, {
2+
FC,
3+
useCallback,
4+
useContext,
5+
useMemo,
6+
useRef,
7+
useState,
8+
} from 'react';
9+
import {Pressable} from 'react-native';
10+
import MapView, {Marker, Polyline} from 'react-native-maps';
11+
import styled from '@emotion/native';
12+
import {ThemeContext} from '@emotion/react';
13+
14+
import {Map} from '../../../components';
15+
import {StoreContext} from '../../../contexts';
16+
import {getBestLap} from '../../../utils/race';
17+
import {timeFormatter} from '../../../utils/formatters';
18+
import {Speed} from './Speed';
19+
import {ITheme} from '../../../themes/Theme.interface';
20+
// @ts-expect-error types
21+
import startMarker from '../../../images/start-marker.png';
22+
import {Store} from '../../../store/Store.types';
23+
import {getPercentageValue} from '../../../utils/utils';
24+
import {colorFromRGB, getGradientColor} from '../../../utils/color';
25+
26+
const Container = styled.ScrollView`
27+
margin-top: 32px;
28+
flex-grow: 1;
29+
`;
30+
const Lap = styled.View`
31+
padding: 4px;
32+
border-bottom-color: ${props => props.theme.accentColor50};
33+
border-bottom-width: 1px;
34+
`;
35+
36+
const LapTime = styled.Text<{best: boolean}>`
37+
font-weight: 400;
38+
font-size: 18px;
39+
color: ${props =>
40+
props.best ? props.theme.positiveColor : props.theme.accentColor50};
41+
`;
42+
43+
const TimeDelta = styled.Text<{best: boolean}>`
44+
font-weight: 400;
45+
font-size: 18px;
46+
color: ${props =>
47+
props.best ? props.theme.accentColor50 : props.theme.negativeColor};
48+
`;
49+
50+
const MapWrap = styled.View`
51+
height: 350px;
52+
border-radius: 4px;
53+
`;
54+
55+
const LapInfo = styled.View`
56+
flex-grow: 1;
57+
`;
58+
59+
interface Props extends Pick<Store, 'laps' | 'startPoint'> {}
60+
61+
export const Laps: FC<Props> = ({laps, startPoint}) => {
62+
const [selectedLap, setSelectedLap] = useState<number>();
63+
const bestLap = useMemo(() => getBestLap(laps), [laps]);
64+
const map = useRef<MapView | null>();
65+
const theme = useContext(ThemeContext);
66+
const getLines = useCallback(
67+
(lap: Store['path']) =>
68+
lap
69+
.map((value, index, array) => {
70+
if (!index) {
71+
return null;
72+
}
73+
return [array[index - 1], value];
74+
})
75+
.filter(line => line),
76+
[],
77+
);
78+
79+
return (
80+
<Container>
81+
{laps.map(lap => (
82+
<Lap key={lap.lapNumber}>
83+
<Pressable
84+
onPress={() => {
85+
setSelectedLap(lap.lapNumber === selectedLap ? 0 : lap.lapNumber);
86+
}}>
87+
<LapTime best={lap.time === bestLap?.time}>
88+
круг {lap.lapNumber}{timeFormatter(lap.time)}{' '}
89+
{lap.lapNumber === selectedLap && (
90+
<TimeDelta best={lap.time === bestLap?.time}>
91+
+{timeFormatter(lap.time - (bestLap?.time ?? lap.time))}
92+
</TimeDelta>
93+
)}
94+
</LapTime>
95+
</Pressable>
96+
<LapInfo>
97+
{lap.lapNumber === selectedLap && (
98+
<>
99+
<Speed bestLap={bestLap} lap={lap} />
100+
101+
<MapWrap>
102+
<Map
103+
customMapStyle={(theme as ITheme).mapStyle}
104+
camera={
105+
startPoint
106+
? {
107+
center: startPoint,
108+
zoom: 15,
109+
heading: 0,
110+
pitch: 0,
111+
}
112+
: undefined
113+
}
114+
innerRef={ref => {
115+
map.current = ref;
116+
}}>
117+
{getLines(lap.path).map((line, index) => (
118+
<Polyline
119+
key={index}
120+
strokeWidth={5}
121+
strokeColor={colorFromRGB(
122+
getGradientColor(
123+
[0, 255, 0],
124+
[255, 0, 0],
125+
getPercentageValue(
126+
lap.minSpeed,
127+
lap.maxSpeed,
128+
line?.[0]?.speed ?? 0,
129+
),
130+
),
131+
)}
132+
coordinates={
133+
line?.map(l => ({
134+
latitude: l?.latitude ?? 0,
135+
longitude: l?.longitude ?? 0,
136+
})) ?? []
137+
}
138+
/>
139+
))}
140+
{startPoint ? (
141+
<Marker coordinate={startPoint} image={startMarker} />
142+
) : null}
143+
</Map>
144+
</MapWrap>
145+
</>
146+
)}
147+
</LapInfo>
148+
</Lap>
149+
))}
150+
</Container>
151+
);
152+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React, {FC} from 'react';
2+
import {numberWithSign, speedFormatter} from '../../../utils/formatters';
3+
import {msTokmh} from '../../../utils/geolocation';
4+
import styled from '@emotion/native/dist/emotion-native.cjs';
5+
import {Lap} from '../../../store/Store.types';
6+
7+
const SpeedWrap = styled.View`
8+
flex-direction: row;
9+
justify-content: space-between;
10+
`;
11+
12+
const Title = styled.Text`
13+
margin-top: 24px;
14+
color: ${props => props.theme.accentColor50};
15+
`;
16+
17+
const Min = styled.Text`
18+
font-weight: 400;
19+
font-size: 22px;
20+
opacity: 0.5;
21+
color: ${props => props.theme.accentColor};
22+
`;
23+
24+
const Current = styled.Text`
25+
font-weight: 400;
26+
font-size: 32px;
27+
color: ${props => props.theme.accentColor};
28+
`;
29+
30+
const Max = styled.Text`
31+
font-weight: 400;
32+
font-size: 22px;
33+
opacity: 0.5;
34+
color: ${props => props.theme.accentColor};
35+
`;
36+
37+
const Delta = styled.Text`
38+
font-size: 12px;
39+
color: ${props =>
40+
+(props?.children ?? 0) < 0
41+
? props.theme.negativeColor
42+
: props.theme.positiveColor};
43+
height: 22px;
44+
vertical-align: top;
45+
`;
46+
47+
const Section = styled.View`
48+
margin-top: 12px;
49+
align-items: center;
50+
`;
51+
52+
interface Props {
53+
bestLap?: Lap;
54+
lap: Lap;
55+
}
56+
57+
export const Speed: FC<Props> = ({bestLap, lap}) => (
58+
<>
59+
<Title>Скорость</Title>
60+
61+
<SpeedWrap>
62+
<Section>
63+
<Min>
64+
{lap.minSpeed >= 0 ? speedFormatter(msTokmh(lap.minSpeed)) : 0}
65+
</Min>
66+
{bestLap && (
67+
<Delta>
68+
{numberWithSign(
69+
+speedFormatter(msTokmh(lap.minSpeed - bestLap.minSpeed)),
70+
)}
71+
</Delta>
72+
)}
73+
</Section>
74+
<Section>
75+
<Current>
76+
{speedFormatter(msTokmh(lap.distance / lap.time))} км/ч
77+
</Current>
78+
</Section>
79+
<Section>
80+
<Max>{speedFormatter(msTokmh(lap.maxSpeed))}</Max>
81+
{bestLap && (
82+
<Delta>
83+
{numberWithSign(
84+
+speedFormatter(msTokmh(lap.maxSpeed - bestLap.maxSpeed)),
85+
)}
86+
</Delta>
87+
)}
88+
</Section>
89+
</SpeedWrap>
90+
</>
91+
);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './BestLap';
2+
export * from './Laps';
3+
export * from './Speed';

src/components/RaceResult/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './RaceResult';

src/hooks/useStoreKey.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import {useEffect, useState} from 'react';
22
import AsyncStorage from '@react-native-async-storage/async-storage';
33

4-
export const useStoreKey = (key: string, defaultValue?: string) => {
4+
export const useStoreKey = <T>(
5+
key: string,
6+
defaultValue?: T,
7+
): T | undefined => {
58
const [value, setValue] = useState<any>();
69

710
useEffect(() => {
811
AsyncStorage.getItem(key).then(val => setValue(val ?? defaultValue));
912
}, [defaultValue, key]);
1013

11-
return value;
14+
try {
15+
return JSON.parse(value);
16+
} catch {
17+
return value;
18+
}
1219
};

src/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {AppRegistry} from 'react-native';
2+
import {setDefaultOptions} from 'date-fns';
3+
import {ru} from 'date-fns/locale';
4+
25
import App from './App';
36

7+
setDefaultOptions({locale: ru});
8+
49
AppRegistry.registerComponent('racy', () => App);
510
AppRegistry.runApplication('racy', {
611
rootTag: document.getElementById('root'),

0 commit comments

Comments
 (0)