Front: Graphes de Score (#248)
This commit is contained in:
78
front/components/ScoreGraph.tsx
Normal file
78
front/components/ScoreGraph.tsx
Normal 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;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user