Merge pull request #257 from Chroma-Case/feature/adc/#224-genre-view
Feature/adc/#224 genre view
This commit is contained in:
@@ -6,6 +6,14 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
export class SongService {
|
export class SongService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async songByArtist(data: number): Promise<Song[]> {
|
||||||
|
return this.prisma.song.findMany({
|
||||||
|
where: {
|
||||||
|
artistId: {equals: data},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
|
async createSong(data: Prisma.SongCreateInput): Promise<Song> {
|
||||||
return this.prisma.song.create({
|
return this.prisma.song.create({
|
||||||
data,
|
data,
|
||||||
|
|||||||
54
front/API.ts
54
front/API.ts
@@ -287,6 +287,43 @@ export default class API {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description retrieves songs from a specific artist
|
||||||
|
* @param artistId is the id of the artist that composed the songs aimed
|
||||||
|
* @returns a Promise of Songs type array
|
||||||
|
*/
|
||||||
|
public static getSongsByArtist(artistId: number): Query<Song[]> {
|
||||||
|
return {
|
||||||
|
key: ['artist', artistId, 'songs'],
|
||||||
|
exec: () =>
|
||||||
|
API.fetch(
|
||||||
|
{
|
||||||
|
route: `/song?artistId=${artistId}`,
|
||||||
|
},
|
||||||
|
{ handler: PlageHandler(SongHandler) }
|
||||||
|
).then(({ data }) => data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all songs corresponding to the given genre ID
|
||||||
|
* @param genreId the id of the genre we're aiming
|
||||||
|
* @returns a promise of an array of Songs
|
||||||
|
*/
|
||||||
|
public static getSongsByGenre(genreId: number): Query<Song[]> {
|
||||||
|
return {
|
||||||
|
key: ['genre', genreId, 'songs'],
|
||||||
|
exec: () =>
|
||||||
|
API.fetch(
|
||||||
|
{
|
||||||
|
route: `/song?genreId=${genreId}`,
|
||||||
|
},
|
||||||
|
{ handler: PlageHandler(SongHandler) }
|
||||||
|
).then(({ data }) => data),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrive a song's midi partition
|
* Retrive a song's midi partition
|
||||||
* @param songId the id to find the song
|
* @param songId the id to find the song
|
||||||
@@ -322,6 +359,23 @@ export default class API {
|
|||||||
return `${API.baseUrl}/genre/${genreId}/illustration`;
|
return `${API.baseUrl}/genre/${genreId}/illustration`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a genre
|
||||||
|
* @param genreId the id of the aimed genre
|
||||||
|
*/
|
||||||
|
public static getGenre(genreId: number): Query<Genre> {
|
||||||
|
return {
|
||||||
|
key: ['genre', genreId],
|
||||||
|
exec: () =>
|
||||||
|
API.fetch(
|
||||||
|
{
|
||||||
|
route: `/genre/${genreId}`,
|
||||||
|
},
|
||||||
|
{ handler: GenreHandler }
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrive a song's musicXML partition
|
* Retrive a song's musicXML partition
|
||||||
* @param songId the id to find the song
|
* @param songId the id to find the song
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { Button, Center, VStack } from 'native-base';
|
|||||||
import { unsetAccessToken } from './state/UserSlice';
|
import { unsetAccessToken } from './state/UserSlice';
|
||||||
import TextButton from './components/TextButton';
|
import TextButton from './components/TextButton';
|
||||||
import ErrorView from './views/ErrorView';
|
import ErrorView from './views/ErrorView';
|
||||||
|
import GenreDetailsView from './views/GenreDetailsView';
|
||||||
import GoogleView from './views/GoogleView';
|
import GoogleView from './views/GoogleView';
|
||||||
|
|
||||||
// Util function to hide route props in URL
|
// Util function to hide route props in URL
|
||||||
@@ -59,6 +60,11 @@ const protectedRoutes = () =>
|
|||||||
options: { title: translate('artistFilter') },
|
options: { title: translate('artistFilter') },
|
||||||
link: '/artist/:artistId',
|
link: '/artist/:artistId',
|
||||||
},
|
},
|
||||||
|
Genre: {
|
||||||
|
component: GenreDetailsView,
|
||||||
|
options: { title: translate('genreFilter') },
|
||||||
|
link: '/genre/:genreId',
|
||||||
|
},
|
||||||
Score: {
|
Score: {
|
||||||
component: ScoreView,
|
component: ScoreView,
|
||||||
options: { title: translate('score'), headerLeft: null },
|
options: { title: translate('score'), headerLeft: null },
|
||||||
|
|||||||
34
front/components/RowCustom.tsx
Normal file
34
front/components/RowCustom.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useColorScheme } from 'react-native';
|
||||||
|
import { RootState, useSelector } from '../state/Store';
|
||||||
|
import { Box, Pressable } from 'native-base';
|
||||||
|
|
||||||
|
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
|
||||||
|
const settings = useSelector((state: RootState) => state.settings.local);
|
||||||
|
const systemColorMode = useColorScheme();
|
||||||
|
const colorScheme = settings.colorScheme;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={props.onPress}>
|
||||||
|
{({ isHovered, isPressed }) => (
|
||||||
|
<Box
|
||||||
|
{...props}
|
||||||
|
py={3}
|
||||||
|
my={1}
|
||||||
|
bg={
|
||||||
|
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
|
||||||
|
? isHovered || isPressed
|
||||||
|
? 'gray.800'
|
||||||
|
: undefined
|
||||||
|
: isHovered || isPressed
|
||||||
|
? 'coolGray.200'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RowCustom;
|
||||||
@@ -1,20 +1,16 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
HStack,
|
|
||||||
VStack,
|
VStack,
|
||||||
Heading,
|
Heading,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Image,
|
|
||||||
Flex,
|
Flex,
|
||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
Column,
|
Column,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'native-base';
|
} from 'native-base';
|
||||||
import { SafeAreaView, useColorScheme } from 'react-native';
|
import { SafeAreaView } from 'react-native';
|
||||||
import { RootState, useSelector } from '../state/Store';
|
|
||||||
import { SearchContext } from '../views/SearchView';
|
import { SearchContext } from '../views/SearchView';
|
||||||
import { useQueries, useQuery } from '../Queries';
|
import { useQueries, useQuery } from '../Queries';
|
||||||
import { translate } from '../i18n/i18n';
|
import { translate } from '../i18n/i18n';
|
||||||
@@ -24,11 +20,11 @@ import ArtistCard from './ArtistCard';
|
|||||||
import GenreCard from './GenreCard';
|
import GenreCard from './GenreCard';
|
||||||
import SongCard from './SongCard';
|
import SongCard from './SongCard';
|
||||||
import CardGridCustom from './CardGridCustom';
|
import CardGridCustom from './CardGridCustom';
|
||||||
import TextButton from './TextButton';
|
|
||||||
import SearchHistoryCard from './HistoryCard';
|
import SearchHistoryCard from './HistoryCard';
|
||||||
import Song, { SongWithArtist } from '../models/Song';
|
import Song, { SongWithArtist } from '../models/Song';
|
||||||
import { useNavigation } from '../Navigation';
|
import { useNavigation } from '../Navigation';
|
||||||
import Artist from '../models/Artist';
|
import Artist from '../models/Artist';
|
||||||
|
import SongRow from '../components/SongRow';
|
||||||
|
|
||||||
const swaToSongCardProps = (song: SongWithArtist) => ({
|
const swaToSongCardProps = (song: SongWithArtist) => ({
|
||||||
songId: song.id,
|
songId: song.id,
|
||||||
@@ -37,101 +33,6 @@ const swaToSongCardProps = (song: SongWithArtist) => ({
|
|||||||
cover: song.cover ?? 'https://picsum.photos/200',
|
cover: song.cover ?? 'https://picsum.photos/200',
|
||||||
});
|
});
|
||||||
|
|
||||||
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
|
|
||||||
const settings = useSelector((state: RootState) => state.settings.local);
|
|
||||||
const systemColorMode = useColorScheme();
|
|
||||||
const colorScheme = settings.colorScheme;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable onPress={props.onPress}>
|
|
||||||
{({ isHovered, isPressed }) => (
|
|
||||||
<Box
|
|
||||||
{...props}
|
|
||||||
py={3}
|
|
||||||
my={1}
|
|
||||||
bg={
|
|
||||||
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
|
|
||||||
? isHovered || isPressed
|
|
||||||
? 'gray.800'
|
|
||||||
: undefined
|
|
||||||
: isHovered || isPressed
|
|
||||||
? 'coolGray.200'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type SongRowProps = {
|
|
||||||
song: Song | SongWithArtist; // TODO: remove Song
|
|
||||||
onPress: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SongRow = ({ song, onPress }: SongRowProps) => {
|
|
||||||
return (
|
|
||||||
<RowCustom width={'100%'}>
|
|
||||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
|
||||||
<Image
|
|
||||||
flexShrink={0}
|
|
||||||
flexGrow={0}
|
|
||||||
pl={10}
|
|
||||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
|
||||||
source={{ uri: song.cover }}
|
|
||||||
alt={song.name}
|
|
||||||
/>
|
|
||||||
<HStack
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexShrink: 1,
|
|
||||||
flexGrow: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
}}
|
|
||||||
space={6}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
flexShrink: 1,
|
|
||||||
}}
|
|
||||||
isTruncated
|
|
||||||
pl={10}
|
|
||||||
maxW={'100%'}
|
|
||||||
bold
|
|
||||||
fontSize="md"
|
|
||||||
>
|
|
||||||
{song.name}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
fontSize={'sm'}
|
|
||||||
>
|
|
||||||
{song.artistId ?? 'artist'}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<TextButton
|
|
||||||
flexShrink={0}
|
|
||||||
flexGrow={0}
|
|
||||||
translate={{ translationKey: 'playBtn' }}
|
|
||||||
colorScheme="primary"
|
|
||||||
variant={'outline'}
|
|
||||||
size="sm"
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</RowCustom>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SongRow.defaultProps = {
|
|
||||||
onPress: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const HomeSearchComponent = () => {
|
const HomeSearchComponent = () => {
|
||||||
const { updateStringQuery } = React.useContext(SearchContext);
|
const { updateStringQuery } = React.useContext(SearchContext);
|
||||||
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
||||||
@@ -252,15 +153,17 @@ const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
{artistData?.length ? (
|
{artistData?.length ? (
|
||||||
<CardGridCustom
|
<CardGridCustom
|
||||||
content={artistData.slice(0, props.maxItems ?? artistData.length).map((a) => ({
|
content={artistData
|
||||||
image: API.getArtistIllustration(a.id),
|
.slice(0, props.maxItems ?? artistData.length)
|
||||||
name: a.name,
|
.map((artistData) => ({
|
||||||
id: a.id,
|
image: API.getArtistIllustration(artistData.id),
|
||||||
onPress: () => {
|
name: artistData.name,
|
||||||
API.createSearchHistoryEntry(a.name, 'artist');
|
id: artistData.id,
|
||||||
navigation.navigate('Artist', { artistId: a.id });
|
onPress: () => {
|
||||||
},
|
API.createSearchHistoryEntry(artistData.name, 'artist');
|
||||||
}))}
|
navigation.navigate('Artist', { artistId: artistData.id });
|
||||||
|
},
|
||||||
|
}))}
|
||||||
cardComponent={ArtistCard}
|
cardComponent={ArtistCard}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -287,7 +190,7 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
|||||||
id: g.id,
|
id: g.id,
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
API.createSearchHistoryEntry(g.name, 'genre');
|
API.createSearchHistoryEntry(g.name, 'genre');
|
||||||
navigation.navigate('Home');
|
navigation.navigate('Genre', { genreId: g.id });
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
cardComponent={GenreCard}
|
cardComponent={GenreCard}
|
||||||
|
|||||||
71
front/components/SongRow.tsx
Normal file
71
front/components/SongRow.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { HStack, Image, Text } from 'native-base';
|
||||||
|
import Song, { SongWithArtist } from '../models/Song';
|
||||||
|
import RowCustom from './RowCustom';
|
||||||
|
import TextButton from './TextButton';
|
||||||
|
|
||||||
|
type SongRowProps = {
|
||||||
|
song: Song | SongWithArtist; // TODO: remove Song
|
||||||
|
onPress: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SongRow = ({ song, onPress }: SongRowProps) => {
|
||||||
|
return (
|
||||||
|
<RowCustom width={'100%'}>
|
||||||
|
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||||
|
<Image
|
||||||
|
flexShrink={0}
|
||||||
|
flexGrow={0}
|
||||||
|
pl={10}
|
||||||
|
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||||
|
source={{ uri: song.cover }}
|
||||||
|
alt={song.name}
|
||||||
|
borderColor={'white'}
|
||||||
|
borderWidth={1}
|
||||||
|
/>
|
||||||
|
<HStack
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexShrink: 1,
|
||||||
|
flexGrow: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
}}
|
||||||
|
space={6}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flexShrink: 1,
|
||||||
|
}}
|
||||||
|
isTruncated
|
||||||
|
pl={5}
|
||||||
|
maxW={'100%'}
|
||||||
|
bold
|
||||||
|
fontSize="md"
|
||||||
|
>
|
||||||
|
{song.name}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
fontSize={'sm'}
|
||||||
|
>
|
||||||
|
{song.artistId ?? 'artist'}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<TextButton
|
||||||
|
flexShrink={0}
|
||||||
|
flexGrow={0}
|
||||||
|
translate={{ translationKey: 'playBtn' }}
|
||||||
|
colorScheme="primary"
|
||||||
|
variant={'outline'}
|
||||||
|
size="sm"
|
||||||
|
mr={5}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</RowCustom>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SongRow;
|
||||||
@@ -43,6 +43,7 @@ export const en = {
|
|||||||
artistFilter: 'Artists',
|
artistFilter: 'Artists',
|
||||||
songsFilter: 'Songs',
|
songsFilter: 'Songs',
|
||||||
genreFilter: 'Genres',
|
genreFilter: 'Genres',
|
||||||
|
favoriteFilter: 'Favorites',
|
||||||
|
|
||||||
// profile page
|
// profile page
|
||||||
user: 'Profile',
|
user: 'Profile',
|
||||||
@@ -231,6 +232,7 @@ export const fr: typeof en = {
|
|||||||
artistFilter: 'Artistes',
|
artistFilter: 'Artistes',
|
||||||
songsFilter: 'Morceaux',
|
songsFilter: 'Morceaux',
|
||||||
genreFilter: 'Genres',
|
genreFilter: 'Genres',
|
||||||
|
favoriteFilter: 'Favoris',
|
||||||
|
|
||||||
// Difficulty settings
|
// Difficulty settings
|
||||||
diffBtn: 'Difficulté',
|
diffBtn: 'Difficulté',
|
||||||
@@ -428,6 +430,7 @@ export const sp: typeof en = {
|
|||||||
artistFilter: 'Artistas',
|
artistFilter: 'Artistas',
|
||||||
songsFilter: 'canciones',
|
songsFilter: 'canciones',
|
||||||
genreFilter: 'géneros',
|
genreFilter: 'géneros',
|
||||||
|
favoriteFilter: 'Favorites',
|
||||||
|
|
||||||
// Difficulty settings
|
// Difficulty settings
|
||||||
diffBtn: 'Dificultad',
|
diffBtn: 'Dificultad',
|
||||||
|
|||||||
@@ -1,49 +1,58 @@
|
|||||||
import { VStack, Image, Heading, IconButton, Icon, Container } from 'native-base';
|
import { Box, Heading, useBreakpointValue, ScrollView } from 'native-base';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { SafeAreaView } from 'react-native';
|
|
||||||
import { useQuery } from '../Queries';
|
import { useQuery } from '../Queries';
|
||||||
import { LoadingView } from '../components/Loading';
|
import { LoadingView } from '../components/Loading';
|
||||||
import API from '../API';
|
import API from '../API';
|
||||||
import { useNavigation } from '../Navigation';
|
import Song from '../models/Song';
|
||||||
|
import SongRow from '../components/SongRow';
|
||||||
const handleFavorite = () => {};
|
import { Key } from 'react';
|
||||||
|
import { RouteProps, useNavigation } from '../Navigation';
|
||||||
|
import { ImageBackground } from 'react-native';
|
||||||
|
|
||||||
type ArtistDetailsViewProps = {
|
type ArtistDetailsViewProps = {
|
||||||
artistId: number;
|
artistId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => {
|
const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) => {
|
||||||
|
const artistQuery = useQuery(API.getArtist(artistId));
|
||||||
|
const songsQuery = useQuery(API.getSongsByArtist(artistId));
|
||||||
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
|
const isMobileView = screenSize == 'small';
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const { isLoading, data: artistData, isError } = useQuery(API.getArtist(artistId));
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (artistQuery.isError || songsQuery.isError) {
|
||||||
|
navigation.navigate('Error');
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
if (!artistQuery.data || songsQuery.data === undefined) {
|
||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
navigation.navigate('Error');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<ScrollView>
|
||||||
<Container m={3}>
|
<ImageBackground
|
||||||
<Image
|
style={{ width: '100%', height: isMobileView ? 200 : 300 }}
|
||||||
source={{ uri: 'https://picsum.photos/200' }}
|
source={{ uri: API.getArtistIllustration(artistQuery.data.id) }}
|
||||||
alt={artistData?.name}
|
></ImageBackground>
|
||||||
size={20}
|
<Box>
|
||||||
borderRadius="full"
|
<Heading mt={-20} ml={3} fontSize={50}>
|
||||||
/>
|
{artistQuery.data.name}
|
||||||
<VStack space={3}>
|
</Heading>
|
||||||
<Heading>{artistData?.name}</Heading>
|
<ScrollView mt={3}>
|
||||||
<IconButton
|
<Box>
|
||||||
icon={<Icon as={Ionicons} name="heart" size={6} color="red.500" />}
|
{songsQuery.data.map((comp: Song, index: Key | null | undefined) => (
|
||||||
onPress={() => handleFavorite()}
|
<SongRow
|
||||||
variant="unstyled"
|
key={index}
|
||||||
_pressed={{ opacity: 0.6 }}
|
song={comp}
|
||||||
/>
|
onPress={() => {
|
||||||
</VStack>
|
API.createSearchHistoryEntry(comp.name, 'song');
|
||||||
</Container>
|
navigation.navigate('Song', { songId: comp.id });
|
||||||
</SafeAreaView>
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</ScrollView>
|
||||||
|
</Box>
|
||||||
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
74
front/views/GenreDetailsView.tsx
Normal file
74
front/views/GenreDetailsView.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Flex, Heading, useBreakpointValue, ScrollView } from 'native-base';
|
||||||
|
import { useQueries, useQuery } from '../Queries';
|
||||||
|
import { LoadingView } from '../components/Loading';
|
||||||
|
import { RouteProps, useNavigation } from '../Navigation';
|
||||||
|
import API from '../API';
|
||||||
|
import CardGridCustom from '../components/CardGridCustom';
|
||||||
|
import SongCard from '../components/SongCard';
|
||||||
|
import { ImageBackground } from 'react-native';
|
||||||
|
|
||||||
|
type GenreDetailsViewProps = {
|
||||||
|
genreId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GenreDetailsView = ({ genreId }: RouteProps<GenreDetailsViewProps>) => {
|
||||||
|
const genreQuery = useQuery(API.getGenre(genreId));
|
||||||
|
const songsQuery = useQuery(API.getSongsByGenre(genreId));
|
||||||
|
const artistQueries = useQueries(
|
||||||
|
songsQuery.data?.map((song) => song.artistId).map((artistId) => API.getArtist(artistId)) ??
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
// Here, .artist will always be defined
|
||||||
|
const songWithArtist = songsQuery?.data
|
||||||
|
?.map((song) => ({
|
||||||
|
...song,
|
||||||
|
artist: artistQueries.find((query) => query.data?.id == song.artistId)?.data,
|
||||||
|
}))
|
||||||
|
.filter((song) => song.artist !== undefined);
|
||||||
|
|
||||||
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
|
const isMobileView = screenSize == 'small';
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
if (genreQuery.isError || songsQuery.isError) {
|
||||||
|
navigation.navigate('Error');
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
if (!genreQuery.data || songsQuery.data === undefined || songWithArtist === undefined) {
|
||||||
|
return <LoadingView />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<ImageBackground
|
||||||
|
style={{ width: '100%', height: isMobileView ? 200 : 300 }}
|
||||||
|
source={{ uri: API.getGenreIllustration(genreQuery.data.id) }}
|
||||||
|
></ImageBackground>
|
||||||
|
<Heading ml={3} fontSize={50}>
|
||||||
|
{genreQuery.data.name}
|
||||||
|
</Heading>
|
||||||
|
<Flex
|
||||||
|
flexWrap="wrap"
|
||||||
|
direction={isMobileView ? 'column' : 'row'}
|
||||||
|
justifyContent={['flex-start']}
|
||||||
|
mt={4}
|
||||||
|
>
|
||||||
|
<CardGridCustom
|
||||||
|
content={songWithArtist.map((songData) => ({
|
||||||
|
name: songData.name,
|
||||||
|
cover: songData.cover,
|
||||||
|
artistName: songData.artist!.name,
|
||||||
|
songId: songData.id,
|
||||||
|
onPress: () => {
|
||||||
|
API.createSearchHistoryEntry(songData.name, 'song');
|
||||||
|
navigation.navigate('Song', { songId: songData.id });
|
||||||
|
},
|
||||||
|
}))}
|
||||||
|
cardComponent={SongCard}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</ScrollView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenreDetailsView;
|
||||||
@@ -9118,6 +9118,11 @@ expo-keep-awake@~11.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
|
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
|
||||||
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
|
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
|
||||||
|
|
||||||
|
expo-linear-gradient@^12.3.0:
|
||||||
|
version "12.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.3.0.tgz#7abd8fedbf0138c86805aebbdfbbf5e5fa865f19"
|
||||||
|
integrity sha512-f9e+Oxe5z7fNQarTBZXilMyswlkbYWQHONVfq8MqmiEnW3h9XsxxmVJLG8uVQSQPUsbW+x1UUT/tnU6mkMWeLg==
|
||||||
|
|
||||||
expo-linking@~3.3.1:
|
expo-linking@~3.3.1:
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
|
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
|
||||||
|
|||||||
Reference in New Issue
Block a user