Front: Graphes de Score (#248)
This commit is contained in:
@@ -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(),
|
songID: yup.number().required(),
|
||||||
userID: yup.number().required(),
|
userID: yup.number().required(),
|
||||||
score: yup.number().required(),
|
score: yup.number().required(),
|
||||||
|
playDate: yup.date().required(),
|
||||||
difficulties: yup.mixed().required(),
|
difficulties: yup.mixed().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ export type SongHistoryItem = {
|
|||||||
songID: number;
|
songID: number;
|
||||||
userID: number;
|
userID: number;
|
||||||
score: number;
|
score: number;
|
||||||
|
playDate: Date;
|
||||||
difficulties: object;
|
difficulties: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -52,12 +52,13 @@
|
|||||||
"react-dom": "18.1.0",
|
"react-dom": "18.1.0",
|
||||||
"react-i18next": "^11.18.3",
|
"react-i18next": "^11.18.3",
|
||||||
"react-native": "0.70.5",
|
"react-native": "0.70.5",
|
||||||
|
"react-native-chart-kit": "^6.12.0",
|
||||||
"react-native-paper": "^4.12.5",
|
"react-native-paper": "^4.12.5",
|
||||||
"react-native-reanimated": "~2.12.0",
|
"react-native-reanimated": "~2.12.0",
|
||||||
"react-native-safe-area-context": "4.4.1",
|
"react-native-safe-area-context": "4.4.1",
|
||||||
"react-native-screens": "~3.18.0",
|
"react-native-screens": "~3.18.0",
|
||||||
"react-native-super-grid": "^4.6.1",
|
"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-testing-library": "^6.0.0",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-web": "~0.18.7",
|
"react-native-web": "~0.18.7",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import CardGridCustom from '../components/CardGridCustom';
|
|||||||
import SongCard from '../components/SongCard';
|
import SongCard from '../components/SongCard';
|
||||||
import { useQueries, useQuery } from '../Queries';
|
import { useQueries, useQuery } from '../Queries';
|
||||||
import { LoadingView } from '../components/Loading';
|
import { LoadingView } from '../components/Loading';
|
||||||
|
import ScoreGraph from '../components/ScoreGraph';
|
||||||
|
|
||||||
type ScoreViewProps = {
|
type ScoreViewProps = {
|
||||||
songId: number;
|
songId: number;
|
||||||
@@ -32,6 +33,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
|||||||
const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), {
|
const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), {
|
||||||
enabled: songQuery.data !== undefined,
|
enabled: songQuery.data !== undefined,
|
||||||
});
|
});
|
||||||
|
const scoresQuery = useQuery(API.getSongHistory(props.songId), { refetchOnWindowFocus: true });
|
||||||
const recommendations = useQuery(API.getSongSuggestions);
|
const recommendations = useQuery(API.getSongSuggestions);
|
||||||
const artistRecommendations = useQueries(
|
const artistRecommendations = useQueries(
|
||||||
recommendations.data
|
recommendations.data
|
||||||
@@ -54,7 +56,7 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
|
<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">
|
<Text bold fontSize="lg">
|
||||||
{songQuery.data.name}
|
{songQuery.data.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -137,6 +139,9 @@ const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
|||||||
</Column>
|
</Column>
|
||||||
</Card>
|
</Card>
|
||||||
</Row>
|
</Row>
|
||||||
|
{scoresQuery.data && (scoresQuery.data?.history?.length ?? 0) > 1 && (
|
||||||
|
<ScoreGraph songHistory={scoresQuery.data} />
|
||||||
|
)}
|
||||||
<CardGridCustom
|
<CardGridCustom
|
||||||
style={{ justifyContent: 'space-evenly' }}
|
style={{ justifyContent: 'space-evenly' }}
|
||||||
content={recommendations.data.map((i) => ({
|
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 { useQuery } from '../Queries';
|
||||||
import LoadingComponent, { LoadingView } from '../components/Loading';
|
import { LoadingView } from '../components/Loading';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { Translate } from '../i18n/i18n';
|
||||||
import { Translate, translate } from '../i18n/i18n';
|
|
||||||
import formatDuration from 'format-duration';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import TextButton from '../components/TextButton';
|
import TextButton from '../components/TextButton';
|
||||||
import { RouteProps, useNavigation } from '../Navigation';
|
import { RouteProps, useNavigation } from '../Navigation';
|
||||||
|
import ScoreGraph from '../components/ScoreGraph';
|
||||||
|
|
||||||
interface SongLobbyProps {
|
interface SongLobbyProps {
|
||||||
// The unique identifier to find a song
|
// The unique identifier to find a song
|
||||||
@@ -15,6 +14,7 @@ interface SongLobbyProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
|
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
|
||||||
|
const rootComponentPadding = 30;
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
// Refetch to update score when coming back from score view
|
// Refetch to update score when coming back from score view
|
||||||
const songQuery = useQuery(API.getSong(props.songId), { refetchOnWindowFocus: true });
|
const songQuery = useQuery(API.getSong(props.songId), { refetchOnWindowFocus: true });
|
||||||
@@ -22,18 +22,13 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
|
|||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
});
|
});
|
||||||
const scoresQuery = useQuery(API.getSongHistory(props.songId), { 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.isLoading || scoresQuery.isLoading) return <LoadingView />;
|
||||||
if (songQuery.isError || scoresQuery.isError) {
|
if (songQuery.isError || scoresQuery.isError) {
|
||||||
navigation.navigate('Error');
|
navigation.navigate('Error');
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box style={{ padding: 30, flexDirection: 'column' }}>
|
<Box style={{ padding: rootComponentPadding, flexDirection: 'column' }}>
|
||||||
<Box style={{ flexDirection: 'row', height: '30%' }}>
|
<Box style={{ flexDirection: 'row', height: '30%' }}>
|
||||||
<Box style={{ flex: 3 }}>
|
<Box style={{ flex: 3 }}>
|
||||||
<Image
|
<Image
|
||||||
@@ -117,42 +112,9 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
|
|||||||
<Text>{scoresQuery.data?.history.at(0)?.score ?? 0}</Text>
|
<Text>{scoresQuery.data?.history.at(0)?.score ?? 0}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}
|
{scoresQuery.data && (scoresQuery.data?.history?.length ?? 0) > 0 && (
|
||||||
<Box flexDirection="row">
|
<ScoreGraph songHistory={scoresQuery.data} />
|
||||||
<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>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+23
-4
@@ -14716,6 +14716,11 @@ path-type@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
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:
|
pbkdf2@^3.0.3:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
|
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
|
||||||
@@ -14839,6 +14844,11 @@ pnp-webpack-plugin@^1.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ts-pnp "^1.1.6"
|
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:
|
polished@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
|
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"
|
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
|
||||||
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
|
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:
|
react-native-codegen@^0.70.6:
|
||||||
version "0.70.6"
|
version "0.70.6"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.70.6.tgz#2ce17d1faad02ad4562345f8ee7cbe6397eda5cb"
|
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:
|
dependencies:
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
react-native-svg@13.4.0:
|
react-native-svg@^13.10.0:
|
||||||
version "13.4.0"
|
version "13.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.4.0.tgz#82399ba0956c454144618aa581e2d748dd3f010a"
|
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-13.10.0.tgz#d3c6222ea9cc1e21e2af0fd59dfbeafe7a3d0dc1"
|
||||||
integrity sha512-B3TwK+H0+JuRhYPzF21AgqMt4fjhCwDZ9QUtwNstT5XcslJBXC0FoTkdZo8IEb1Sv4suSqhZwlAY6lwOv3tHag==
|
integrity sha512-D/oYTmUi5nsA/2Nw4WYlF1UUi3vZqhpESpiEhpYCIFB/EMd6vz4A/uq3tIzZFcfa5z2oAdGSxRU1TaYr8IcPlQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
css-select "^5.1.0"
|
css-select "^5.1.0"
|
||||||
css-tree "^1.1.3"
|
css-tree "^1.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user