Front: Graphes de Score (#248)

This commit is contained in:
Arthur Jamet
2023-07-26 12:00:06 +01:00
committed by GitHub
parent 567d3250e2
commit 20eb62d19b
6 changed files with 120 additions and 53 deletions

View File

@@ -0,0 +1,78 @@
import { Box, useBreakpointValue, useTheme } from 'native-base';
import { LineChart } from 'react-native-chart-kit';
import { CardBorderRadius } from './Card';
import SongHistory from '../models/SongHistory';
import { useState } from 'react';
type ScoreGraphProps = {
// The result of the call to API.getSongHistory
songHistory: SongHistory;
};
const formatScoreDate = (playDate: Date): string => {
const pad = (n: number) => n.toString().padStart(2, '0');
const formattedDate = `${pad(playDate.getDay())}/${pad(playDate.getMonth())}`;
const formattedTime = `${pad(playDate.getHours())}:${pad(playDate.getMinutes())}`;
return `${formattedDate} ${formattedTime}`;
};
const ScoreGraph = (props: ScoreGraphProps) => {
const theme = useTheme();
const [containerWidth, setContainerWidth] = useState(0);
// We sort the scores by date, asc.
// By default, the API returns them in desc.
// const pointsToDisplay = props.width / 100;
const isSmall = useBreakpointValue({ base: true, md: false });
const scores = props.songHistory.history
.sort((a, b) => {
if (a.playDate < b.playDate) {
return -1;
} else if (a.playDate > b.playDate) {
return 1;
}
return 0;
})
.slice(-10);
return (
<Box
bgColor={theme.colors.primary[500]}
style={{ width: '100%', borderRadius: CardBorderRadius }}
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
>
<LineChart
data={{
labels: isSmall ? [] : scores.map(({ playDate }) => formatScoreDate(playDate)),
datasets: [
{
data: scores.map(({ score }) => score),
},
],
}}
width={containerWidth}
height={200} // Completely arbitrary
transparent={true}
yAxisSuffix=" pts"
chartConfig={{
decimalPlaces: 0,
color: (opacity = 1) => `rgba(255, 255, 255, ${opacity})`,
labelColor: () => theme.colors.white,
propsForDots: {
r: '6',
strokeWidth: '2',
},
}}
bezier
style={{
margin: 3,
shadowColor: theme.colors.primary[400],
shadowOpacity: 1,
shadowRadius: 20,
borderRadius: CardBorderRadius,
}}
/>
</Box>
);
};
export default ScoreGraph;

View File

@@ -5,6 +5,7 @@ export const SongHistoryItemValidator = yup.object({
songID: yup.number().required(),
userID: yup.number().required(),
score: yup.number().required(),
playDate: yup.date().required(),
difficulties: yup.mixed().required(),
});
@@ -38,6 +39,7 @@ export type SongHistoryItem = {
songID: number;
userID: number;
score: number;
playDate: Date;
difficulties: object;
};

View File

@@ -52,12 +52,13 @@
"react-dom": "18.1.0",
"react-i18next": "^11.18.3",
"react-native": "0.70.5",
"react-native-chart-kit": "^6.12.0",
"react-native-paper": "^4.12.5",
"react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "~3.18.0",
"react-native-super-grid": "^4.6.1",
"react-native-svg": "13.4.0",
"react-native-svg": "^13.10.0",
"react-native-testing-library": "^6.0.0",
"react-native-url-polyfill": "^1.3.0",
"react-native-web": "~0.18.7",

View File

@@ -8,6 +8,7 @@ import CardGridCustom from '../components/CardGridCustom';
import SongCard from '../components/SongCard';
import { useQueries, useQuery } from '../Queries';
import { LoadingView } from '../components/Loading';
import ScoreGraph from '../components/ScoreGraph';
type ScoreViewProps = {
songId: number;
@@ -32,6 +33,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), {
enabled: songQuery.data !== undefined,
});
const scoresQuery = useQuery(API.getSongHistory(props.songId), { refetchOnWindowFocus: true });
const recommendations = useQuery(API.getSongSuggestions);
const artistRecommendations = useQueries(
recommendations.data
@@ -54,7 +56,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
return (
<ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: '100%', lg: '50%' }} textAlign="center">
<VStack width={{ base: '100%', lg: '50%' }} space={3} textAlign="center">
<Text bold fontSize="lg">
{songQuery.data.name}
</Text>
@@ -137,6 +139,9 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
</Column>
</Card>
</Row>
{scoresQuery.data && (scoresQuery.data?.history?.length ?? 0) > 1 && (
<ScoreGraph songHistory={scoresQuery.data} />
)}
<CardGridCustom
style={{ justifyContent: 'space-evenly' }}
content={recommendations.data.map((i) => ({

View File

@@ -1,13 +1,12 @@
import { Divider, Box, Image, Text, VStack, PresenceTransition, Icon, Stack } from 'native-base';
import { Box, Image, Text, Icon, Stack } from 'native-base';
import { useQuery } from '../Queries';
import LoadingComponent, { LoadingView } from '../components/Loading';
import React, { useEffect, useState } from 'react';
import { Translate, translate } from '../i18n/i18n';
import formatDuration from 'format-duration';
import { LoadingView } from '../components/Loading';
import { Translate } from '../i18n/i18n';
import { Ionicons } from '@expo/vector-icons';
import API from '../API';
import TextButton from '../components/TextButton';
import { RouteProps, useNavigation } from '../Navigation';
import ScoreGraph from '../components/ScoreGraph';
interface SongLobbyProps {
// The unique identifier to find a song
@@ -15,6 +14,7 @@ interface SongLobbyProps {
}
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
const rootComponentPadding = 30;
const navigation = useNavigation();
// Refetch to update score when coming back from score view
const songQuery = useQuery(API.getSong(props.songId), { refetchOnWindowFocus: true });
@@ -22,18 +22,13 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
refetchOnWindowFocus: true,
});
const scoresQuery = useQuery(API.getSongHistory(props.songId), { refetchOnWindowFocus: true });
const [chaptersOpen, setChaptersOpen] = useState(false);
useEffect(() => {
if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch();
}, [chaptersOpen]);
useEffect(() => {}, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading) return <LoadingView />;
if (songQuery.isError || scoresQuery.isError) {
navigation.navigate('Error');
return <></>;
}
return (
<Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ padding: rootComponentPadding, flexDirection: 'column' }}>
<Box style={{ flexDirection: 'row', height: '30%' }}>
<Box style={{ flex: 3 }}>
<Image
@@ -117,42 +112,9 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
<Text>{scoresQuery.data?.history.at(0)?.score ?? 0}</Text>
</Box>
</Box>
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}
<Box flexDirection="row">
<TextButton
translate={{ translationKey: 'chapters' }}
variant="ghost"
onPress={() => setChaptersOpen(!chaptersOpen)}
endIcon={
<Icon
as={Ionicons}
name={chaptersOpen ? 'chevron-up-outline' : 'chevron-down-outline'}
/>
}
/>
</Box>
<PresenceTransition visible={chaptersOpen} initial={{ opacity: 0 }}>
{chaptersQuery.isLoading && <LoadingComponent />}
{!chaptersQuery.isLoading && (
<VStack flex={1} space={4} padding="4" divider={<Divider />}>
{chaptersQuery.data!.map((chapter) => (
<Box
key={chapter.id}
flexGrow={1}
flexDirection="row"
justifyContent="space-between"
>
<Text>{chapter.name}</Text>
<Text>
{`${translate('level')} ${
chapter.difficulty
} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
</Text>
</Box>
))}
</VStack>
)}
</PresenceTransition>
{scoresQuery.data && (scoresQuery.data?.history?.length ?? 0) > 0 && (
<ScoreGraph songHistory={scoresQuery.data} />
)}
</Box>
);
};

View File

@@ -14716,6 +14716,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
paths-js@^0.4.10:
version "0.4.11"
resolved "https://registry.yarnpkg.com/paths-js/-/paths-js-0.4.11.tgz#b2a9d5f94ee9949aa8fee945f78a12abff44599e"
integrity sha512-3mqcLomDBXOo7Fo+UlaenG6f71bk1ZezPQy2JCmYHy2W2k5VKpP+Jbin9H0bjXynelTbglCqdFhSEkeIkKTYUA==
pbkdf2@^3.0.3:
version "3.1.2"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
@@ -14839,6 +14844,11 @@ pnp-webpack-plugin@^1.5.0:
dependencies:
ts-pnp "^1.1.6"
point-in-polygon@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz#b0af2616c01bdee341cbf2894df643387ca03357"
integrity sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==
polished@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
@@ -15747,6 +15757,15 @@ react-merge-refs@^1.0.0:
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
react-native-chart-kit@^6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz#187a4987a668a85b7e93588c248ed2c33b3a06f6"
integrity sha512-nZLGyCFzZ7zmX0KjYeeSV1HKuPhl1wOMlTAqa0JhlyW62qV/1ZPXHgT8o9s8mkFaGxdqbspOeuaa6I9jUQDgnA==
dependencies:
lodash "^4.17.13"
paths-js "^0.4.10"
point-in-polygon "^1.0.1"
react-native-codegen@^0.70.6:
version "0.70.6"
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.70.6.tgz#2ce17d1faad02ad4562345f8ee7cbe6397eda5cb"
@@ -15816,10 +15835,10 @@ react-native-super-grid@^4.6.1:
dependencies:
prop-types "^15.6.0"
react-native-svg@13.4.0:
version "13.4.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.4.0.tgz#82399ba0956c454144618aa581e2d748dd3f010a"
integrity sha512-B3TwK+H0+JuRhYPzF21AgqMt4fjhCwDZ9QUtwNstT5XcslJBXC0FoTkdZo8IEb1Sv4suSqhZwlAY6lwOv3tHag==
react-native-svg@^13.10.0:
version "13.10.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.10.0.tgz#d3c6222ea9cc1e21e2af0fd59dfbeafe7a3d0dc1"
integrity sha512-D/oYTmUi5nsA/2Nw4WYlF1UUi3vZqhpESpiEhpYCIFB/EMd6vz4A/uq3tIzZFcfa5z2oAdGSxRU1TaYr8IcPlQ==
dependencies:
css-select "^5.1.0"
css-tree "^1.1.3"