Front: Use history include to get best/last score for a song

This commit is contained in:
Arthur Jamet
2023-12-19 11:13:25 +01:00
committed by Clément Le Bihan
parent b4f04f9b71
commit 7a6dc8b0c9
7 changed files with 38 additions and 35 deletions
+5 -9
View File
@@ -20,14 +20,11 @@ export interface MusicItemType {
/** The URL for the song's cover image. */ /** The URL for the song's cover image. */
image: string; image: string;
/** The level of the song difficulty . */
level: number;
/** The last score achieved for this song. */ /** The last score achieved for this song. */
lastScore: number; lastScore: number | null | undefined;
/** The highest score achieved for this song. */ /** The highest score achieved for this song. */
bestScore: number; bestScore: number | null | undefined;
/** Indicates whether the song is liked/favorited by the user. */ /** Indicates whether the song is liked/favorited by the user. */
liked: boolean; liked: boolean;
@@ -141,9 +138,8 @@ function MusicItemComponent(props: MusicItemType) {
); );
// Memoizing formatted numbers to avoid unnecessary computations. // Memoizing formatted numbers to avoid unnecessary computations.
const formattedLevel = useMemo(() => formatNumber(props.level), [props.level]); const formattedLastScore = useMemo(() => formatNumber(props.lastScore ?? 0), [props.lastScore]);
const formattedLastScore = useMemo(() => formatNumber(props.lastScore), [props.lastScore]); const formattedBestScore = useMemo(() => formatNumber(props.bestScore ?? 0), [props.bestScore]);
const formattedBestScore = useMemo(() => formatNumber(props.bestScore), [props.bestScore]);
return ( return (
<HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}> <HStack space={screenSize === 'xl' ? 2 : 1} style={[styles.container, props.style]}>
@@ -179,7 +175,7 @@ function MusicItemComponent(props: MusicItemType) {
/> />
</Row> </Row>
</Column> </Column>
{[formattedLevel, formattedLastScore, formattedBestScore].map((value, index) => ( {[formattedLastScore, formattedBestScore].map((value, index) => (
<Text key={index} style={styles.stats}> <Text key={index} style={styles.stats}>
{value} {value}
</Text> </Text>
+1 -2
View File
@@ -3,7 +3,7 @@ import { FlatList, HStack, View, useBreakpointValue, useTheme, Text, Row } from
import { ActivityIndicator, StyleSheet } from 'react-native'; import { ActivityIndicator, StyleSheet } from 'react-native';
import MusicItem, { MusicItemType } from './MusicItem'; import MusicItem, { MusicItemType } from './MusicItem';
import ButtonBase from './ButtonBase'; import ButtonBase from './ButtonBase';
import { ArrowDown2, Chart2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native'; import { ArrowDown2, ArrowRotateLeft, Cup, Icon } from 'iconsax-react-native';
import { translate } from '../../i18n/i18n'; import { translate } from '../../i18n/i18n';
// Props type definition for MusicItemTitle. // Props type definition for MusicItemTitle.
@@ -176,7 +176,6 @@ function MusicListComponent({
{translate('musicListTitleSong')} {translate('musicListTitleSong')}
</Text> </Text>
{[ {[
{ text: translate('musicListTitleLevel'), icon: Chart2 },
{ text: translate('musicListTitleLastScore'), icon: ArrowRotateLeft }, { text: translate('musicListTitleLastScore'), icon: ArrowRotateLeft },
{ text: translate('musicListTitleBestScore'), icon: Cup }, { text: translate('musicListTitleBestScore'), icon: Cup },
].map((value) => ( ].map((value) => (
+12 -15
View File
@@ -10,21 +10,6 @@ type SongCardInfoProps = {
onPlay: () => void; onPlay: () => void;
}; };
const Scores = [
{
icon: 'warning',
score: 3,
},
{
icon: 'star',
score: -225,
},
{
icon: 'trophy',
score: 100,
},
];
const SongCardInfo = (props: SongCardInfoProps) => { const SongCardInfo = (props: SongCardInfoProps) => {
const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isPhone = screenSize === 'small'; const isPhone = screenSize === 'small';
@@ -37,6 +22,18 @@ const SongCardInfo = (props: SongCardInfoProps) => {
width: isPhone ? 160 : 200, width: isPhone ? 160 : 200,
}; };
const Scores = [
{
icon: 'time',
score: props.song.lastScore ?? '?',
},
{
icon: 'trophy',
score: props.song.bestScore ?? '?',
},
];
return ( return (
<View <View
style={{ style={{
+16 -2
View File
@@ -6,6 +6,7 @@ import ResponseHandler from './ResponseHandler';
import API from '../API'; import API from '../API';
import { AlbumValidator } from './Album'; import { AlbumValidator } from './Album';
import { GenreValidator } from './Genre'; import { GenreValidator } from './Genre';
import { SongHistoryItem, SongHistoryItemValidator } from './SongHistory';
export type SongInclude = 'artist' | 'album' | 'genre' | 'SongHistory' | 'likedByUsers'; export type SongInclude = 'artist' | 'album' | 'genre' | 'SongHistory' | 'likedByUsers';
@@ -20,20 +21,33 @@ export const SongValidator = yup
difficulties: SongDetailsValidator.required(), difficulties: SongDetailsValidator.required(),
details: SongDetailsValidator.required(), details: SongDetailsValidator.required(),
cover: yup.string().required(), cover: yup.string().required(),
SongHistory: yup
.lazy(() => yup.array(SongHistoryItemValidator.default(undefined)))
.optional(),
bestScore: yup.number().optional().nullable(),
lastScore: yup.number().optional().nullable(),
artist: yup.lazy(() => ArtistValidator.default(undefined)).optional(), artist: yup.lazy(() => ArtistValidator.default(undefined)).optional(),
album: yup.lazy(() => AlbumValidator.default(undefined)).optional(), album: yup.lazy(() => AlbumValidator.default(undefined)).optional(),
genre: yup.lazy(() => GenreValidator.default(undefined)).optional(), genre: yup.lazy(() => GenreValidator.default(undefined)).optional(),
}) })
.concat(ModelValidator) .concat(ModelValidator)
.transform((song: Song) => ({ .transform((song: Song & { SongHistory: SongHistoryItem[] }) => ({
...song, ...song,
cover: `${API.baseUrl}/song/${song.id}/illustration`, cover: `${API.baseUrl}/song/${song.id}/illustration`,
details: song.difficulties, details: song.difficulties,
bestScore:
song.SongHistory?.map(({ info }) => info.score)
.sort()
.at(-1) ?? null,
lastScore:
song.SongHistory?.map(({ info, playDate }) => ({ info, playDate }))
.sort((a, b) => yup.date().cast(a.playDate)!.getTime() - yup.date().cast(b.playDate)!.getTime())
.at(0)?.info.score ?? null,
})); }));
export type Song = yup.InferType<typeof SongValidator>; export type Song = yup.InferType<typeof SongValidator>;
export const SongHandler: ResponseHandler<Song> = { export const SongHandler: ResponseHandler<yup.InferType<typeof SongValidator>> = {
validator: SongValidator, validator: SongValidator,
}; };
-2
View File
@@ -1,12 +1,10 @@
import * as yup from 'yup'; import * as yup from 'yup';
import ResponseHandler from './ResponseHandler'; import ResponseHandler from './ResponseHandler';
import { ModelValidator } from './Model'; import { ModelValidator } from './Model';
import { SongValidator } from './Song';
export const SongHistoryItemValidator = yup export const SongHistoryItemValidator = yup
.object({ .object({
songID: yup.number().required(), songID: yup.number().required(),
song: SongValidator.optional().default(undefined),
userID: yup.number().required(), userID: yup.number().required(),
info: yup info: yup
.object({ .object({
+3 -4
View File
@@ -21,16 +21,15 @@ import { LoadingView } from '../components/Loading';
export const FavoritesMusic = () => { export const FavoritesMusic = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const likedSongs = useQuery(API.getLikedSongs(['artist'])); const likedSongs = useQuery(API.getLikedSongs(['artist', 'SongHistory']));
const musics = const musics =
likedSongs.data?.map((x) => ({ likedSongs.data?.map((x) => ({
artist: x.song.artist!.name, artist: x.song.artist!.name,
song: x.song.name, song: x.song.name,
image: x.song.cover, image: x.song.cover,
level: 42, lastScore: x.song.lastScore,
lastScore: 42, bestScore: x.song.bestScore,
bestScore: 42,
liked: true, liked: true,
onLike: () => { onLike: () => {
console.log('onLike'); console.log('onLike');
+1 -1
View File
@@ -10,7 +10,7 @@ import GoldenRatio from '../../components/V2/GoldenRatio';
// eslint-disable-next-line @typescript-eslint/ban-types // eslint-disable-next-line @typescript-eslint/ban-types
const HomeView = (props: RouteProps<{}>) => { const HomeView = (props: RouteProps<{}>) => {
const suggestionsQuery = useQuery(API.getSongSuggestions(['artist'])); const suggestionsQuery = useQuery(API.getSongSuggestions(['artist', 'SongHistory']));
const navigation = useNavigation(); const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: 'small', md: 'big' }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isPhone = screenSize === 'small'; const isPhone = screenSize === 'small';