Merge pull request #358 from Chroma-Case/feat/adc/search-view-v2
Feat/adc/search view v2
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
FROM node:17
|
FROM node:18.10.0
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||||
|
|||||||
10761
back/package-lock.json
generated
10761
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get } from "@nestjs/common";
|
import { Controller, Get, Put } from "@nestjs/common";
|
||||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ScoresService } from "./scores.service";
|
import { ScoresService } from "./scores.service";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
@@ -13,4 +13,10 @@ export class ScoresController {
|
|||||||
getTopTwenty(): Promise<User[]> {
|
getTopTwenty(): Promise<User[]> {
|
||||||
return this.scoresService.topTwenty();
|
return this.scoresService.topTwenty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
|
||||||
|
// @Put("/add")
|
||||||
|
// addScore(): Promise<void> {
|
||||||
|
// return this.ScoresService.add()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
103
front/API.ts
103
front/API.ts
@@ -1,5 +1,4 @@
|
|||||||
import Artist, { ArtistHandler } from './models/Artist';
|
import Artist, { ArtistHandler } from './models/Artist';
|
||||||
import Album from './models/Album';
|
|
||||||
import Chapter from './models/Chapter';
|
import Chapter from './models/Chapter';
|
||||||
import Lesson from './models/Lesson';
|
import Lesson from './models/Lesson';
|
||||||
import Genre, { GenreHandler } from './models/Genre';
|
import Genre, { GenreHandler } from './models/Genre';
|
||||||
@@ -24,6 +23,7 @@ import * as yup from 'yup';
|
|||||||
import { base64ToBlob } from './utils/base64ToBlob';
|
import { base64ToBlob } from './utils/base64ToBlob';
|
||||||
import { ImagePickerAsset } from 'expo-image-picker';
|
import { ImagePickerAsset } from 'expo-image-picker';
|
||||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||||
|
import { searchProps } from './views/V2/SearchView';
|
||||||
|
|
||||||
type AuthenticationInput = { username: string; password: string };
|
type AuthenticationInput = { username: string; password: string };
|
||||||
type RegistrationInput = AuthenticationInput & { email: string };
|
type RegistrationInput = AuthenticationInput & { email: string };
|
||||||
@@ -497,84 +497,6 @@ export default class API {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search a song by its name
|
|
||||||
* @param query the string used to find the songs
|
|
||||||
*/
|
|
||||||
public static searchSongs(query: string): Query<Song[]> {
|
|
||||||
return {
|
|
||||||
key: ['search', 'song', query],
|
|
||||||
exec: () =>
|
|
||||||
API.fetch(
|
|
||||||
{
|
|
||||||
route: `/search/songs/${query}`,
|
|
||||||
},
|
|
||||||
{ handler: ListHandler(SongHandler) }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search artists by name
|
|
||||||
* @param query the string used to find the artists
|
|
||||||
*/
|
|
||||||
public static searchArtists(query: string): Query<Artist[]> {
|
|
||||||
return {
|
|
||||||
key: ['search', 'artist', query],
|
|
||||||
exec: () =>
|
|
||||||
API.fetch(
|
|
||||||
{
|
|
||||||
route: `/search/artists/${query}`,
|
|
||||||
},
|
|
||||||
{ handler: ListHandler(ArtistHandler) }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search Album by name
|
|
||||||
* @param query the string used to find the album
|
|
||||||
*/
|
|
||||||
public static searchAlbum(query: string): Query<Album[]> {
|
|
||||||
return {
|
|
||||||
key: ['search', 'album', query],
|
|
||||||
exec: async () => [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Super Trooper',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Kingdom Heart 365/2 OST',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Random Access Memories',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve music genres
|
|
||||||
*/
|
|
||||||
public static searchGenres(query: string): Query<Genre[]> {
|
|
||||||
return {
|
|
||||||
key: ['search', 'genre', query],
|
|
||||||
exec: () =>
|
|
||||||
API.fetch(
|
|
||||||
{
|
|
||||||
route: `/search/genres/${query}`,
|
|
||||||
},
|
|
||||||
{ handler: ListHandler(GenreHandler) }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a lesson
|
* Retrieve a lesson
|
||||||
* @param lessonId the id to find the lesson
|
* @param lessonId the id to find the lesson
|
||||||
@@ -780,6 +702,29 @@ export default class API {
|
|||||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static searchSongs(query: searchProps, include?: SongInclude[]): Query<Song[]> {
|
||||||
|
const queryParams: string[] = [];
|
||||||
|
|
||||||
|
if (query.query) queryParams.push(`q=${encodeURIComponent(query.query)}`);
|
||||||
|
if (query.artist) queryParams.push(`artistId=${query.artist}`);
|
||||||
|
if (query.genre) queryParams.push(`genreId=${query.genre}`);
|
||||||
|
if (include) queryParams.push(`include=${include.join(',')}`);
|
||||||
|
|
||||||
|
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: ['search', query.query, query.artist, query.genre, include],
|
||||||
|
exec: () => {
|
||||||
|
return API.fetch(
|
||||||
|
{
|
||||||
|
route: `/search/songs${queryString}`,
|
||||||
|
},
|
||||||
|
{ handler: ListHandler(SongHandler) }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static getPartitionMelodyUrl(songId: number): string {
|
public static getPartitionMelodyUrl(songId: number): string {
|
||||||
return `${API.baseUrl}/song/${songId}/assets/melody`;
|
return `${API.baseUrl}/song/${songId}/assets/melody`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const Graph = ({ songId, since }: GraphProps) => {
|
|||||||
|
|
||||||
const ScoreGraph = () => {
|
const ScoreGraph = () => {
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions();
|
||||||
const songs = useQuery(API.getAllSongs);
|
const songs = useQuery(API.getAllSongs());
|
||||||
const rangeOptions = [
|
const rangeOptions = [
|
||||||
{ label: '3 derniers jours', value: '3days' },
|
{ label: '3 derniers jours', value: '3days' },
|
||||||
{ label: 'Dernière semaine', value: 'week' },
|
{ label: 'Dernière semaine', value: 'week' },
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
import { Icon, Input, Button, Flex } from 'native-base';
|
|
||||||
import React from 'react';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { translate } from '../i18n/i18n';
|
|
||||||
import { SearchContext } from '../views/SearchView';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
|
|
||||||
export type Filter = 'artist' | 'song' | 'genre' | 'all' | 'favorites';
|
|
||||||
|
|
||||||
type FilterButton = {
|
|
||||||
name: string;
|
|
||||||
callback: () => void;
|
|
||||||
id: Filter;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SearchBar = () => {
|
|
||||||
const { filter, updateFilter } = React.useContext(SearchContext);
|
|
||||||
const { stringQuery, updateStringQuery } = React.useContext(SearchContext);
|
|
||||||
const [barText, updateBarText] = React.useState(stringQuery);
|
|
||||||
|
|
||||||
const debouncedUpdateStringQuery = debounce(updateStringQuery, 500);
|
|
||||||
|
|
||||||
// there's a bug due to recursive feedback that erase the text as soon as you type this is a temporary "fix"
|
|
||||||
// will probably be fixed by removing the React.useContext
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// updateBarText(stringQuery);
|
|
||||||
// }, [stringQuery]);
|
|
||||||
|
|
||||||
const handleClearQuery = () => {
|
|
||||||
updateStringQuery('');
|
|
||||||
updateBarText('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeText = (text: string) => {
|
|
||||||
debouncedUpdateStringQuery(text);
|
|
||||||
updateBarText(text);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filters: FilterButton[] = [
|
|
||||||
{
|
|
||||||
name: translate('allFilter'),
|
|
||||||
callback: () => updateFilter('all'),
|
|
||||||
id: 'all',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: translate('favoriteFilter'),
|
|
||||||
callback: () => updateFilter('favorites'),
|
|
||||||
id: 'favorites',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: translate('artistFilter'),
|
|
||||||
callback: () => updateFilter('artist'),
|
|
||||||
id: 'artist',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: translate('songsFilter'),
|
|
||||||
callback: () => updateFilter('song'),
|
|
||||||
id: 'song',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: translate('genreFilter'),
|
|
||||||
callback: () => updateFilter('genre'),
|
|
||||||
id: 'genre',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex m={3} flexDirection={['column', 'row']}>
|
|
||||||
<Input
|
|
||||||
onChangeText={(text) => handleChangeText(text)}
|
|
||||||
variant={'rounded'}
|
|
||||||
value={barText}
|
|
||||||
rounded={'full'}
|
|
||||||
placeholder={translate('search')}
|
|
||||||
width={['100%', '50%']} //responsive array syntax with native-base
|
|
||||||
py={2}
|
|
||||||
px={2}
|
|
||||||
fontSize={'12'}
|
|
||||||
InputLeftElement={
|
|
||||||
<Icon
|
|
||||||
m={[1, 2]}
|
|
||||||
ml={[2, 3]}
|
|
||||||
size={['4', '6']}
|
|
||||||
color="gray.400"
|
|
||||||
as={<MaterialIcons name="search" />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
InputRightElement={
|
|
||||||
<Icon
|
|
||||||
m={[1, 2]}
|
|
||||||
mr={[2, 3]}
|
|
||||||
size={['4', '6']}
|
|
||||||
color="gray.400"
|
|
||||||
onPress={handleClearQuery}
|
|
||||||
as={<MaterialIcons name="close" />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Flex flexDirection={'row'}>
|
|
||||||
{filters.map((btn) => (
|
|
||||||
<Button
|
|
||||||
key={btn.name}
|
|
||||||
rounded={'full'}
|
|
||||||
onPress={btn.callback}
|
|
||||||
mx={[2, 5]}
|
|
||||||
my={[1, 0]}
|
|
||||||
minW={[30, 20]}
|
|
||||||
variant={filter === btn.id ? 'solid' : 'outline'}
|
|
||||||
>
|
|
||||||
{btn.name}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchBar;
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
VStack,
|
|
||||||
Heading,
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
Flex,
|
|
||||||
useBreakpointValue,
|
|
||||||
Column,
|
|
||||||
ScrollView,
|
|
||||||
} from 'native-base';
|
|
||||||
import { SafeAreaView } from 'react-native';
|
|
||||||
import { SearchContext } from '../views/SearchView';
|
|
||||||
import { useQuery } from '../Queries';
|
|
||||||
import { Translate, translate } from '../i18n/i18n';
|
|
||||||
import API from '../API';
|
|
||||||
import LoadingComponent, { LoadingView } from './Loading';
|
|
||||||
import ArtistCard from './ArtistCard';
|
|
||||||
import GenreCard from './GenreCard';
|
|
||||||
import SongCard from './SongCard';
|
|
||||||
import CardGridCustom from './CardGridCustom';
|
|
||||||
import SearchHistoryCard from './HistoryCard';
|
|
||||||
import Song from '../models/Song';
|
|
||||||
import { useNavigation } from '../Navigation';
|
|
||||||
import SongRow from '../components/SongRow';
|
|
||||||
import FavSongRow from './FavSongRow';
|
|
||||||
import { useLikeSongMutation } from '../utils/likeSongMutation';
|
|
||||||
|
|
||||||
const swaToSongCardProps = (song: Song) => ({
|
|
||||||
songId: song.id,
|
|
||||||
name: song.name,
|
|
||||||
artistName: song.artist!.name,
|
|
||||||
cover: song.cover,
|
|
||||||
});
|
|
||||||
|
|
||||||
const HomeSearchComponent = () => {
|
|
||||||
const { updateStringQuery } = React.useContext(SearchContext);
|
|
||||||
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
|
||||||
API.getSearchHistory(0, 12),
|
|
||||||
{ enabled: true }
|
|
||||||
);
|
|
||||||
const songSuggestions = useQuery(API.getSongSuggestions(['artist']));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
|
||||||
<Card shadow={3} mb={5}>
|
|
||||||
<Heading margin={5}>{translate('lastSearched')}</Heading>
|
|
||||||
{isLoadingHistory ? (
|
|
||||||
<LoadingComponent />
|
|
||||||
) : (
|
|
||||||
<CardGridCustom
|
|
||||||
content={historyData.map((h) => {
|
|
||||||
return {
|
|
||||||
...h,
|
|
||||||
timestamp: h.timestamp.toLocaleString(),
|
|
||||||
onPress: () => {
|
|
||||||
updateStringQuery(h.query);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})}
|
|
||||||
cardComponent={SearchHistoryCard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
<Card shadow={3} mt={5} mb={5}>
|
|
||||||
<Heading margin={5}>{translate('songsToGetBetter')}</Heading>
|
|
||||||
{!songSuggestions.data ? (
|
|
||||||
<LoadingComponent />
|
|
||||||
) : (
|
|
||||||
<CardGridCustom
|
|
||||||
content={songSuggestions.data.map(swaToSongCardProps)}
|
|
||||||
cardComponent={SongCard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type SongsSearchComponentProps = {
|
|
||||||
maxRows?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const { songData } = React.useContext(SearchContext);
|
|
||||||
const favoritesQuery = useQuery(API.getLikedSongs(['artist']));
|
|
||||||
const { mutate } = useLikeSongMutation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView>
|
|
||||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
|
||||||
<Box>
|
|
||||||
{songData?.length ? (
|
|
||||||
songData.slice(0, props.maxRows).map((comp, index) => (
|
|
||||||
<SongRow
|
|
||||||
key={index}
|
|
||||||
song={comp}
|
|
||||||
isLiked={
|
|
||||||
!favoritesQuery.data?.find((query) => query?.songId == comp.id)
|
|
||||||
}
|
|
||||||
handleLike={async (state: boolean, songId: number) =>
|
|
||||||
mutate({ songId: songId, like: state })
|
|
||||||
}
|
|
||||||
onPress={() => {
|
|
||||||
API.createSearchHistoryEntry(comp.name, 'song');
|
|
||||||
navigation.navigate('Play', { songId: comp.id });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Translate translationKey="errNoResults" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ItemSearchComponentProps = {
|
|
||||||
maxItems?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
|
|
||||||
const { artistData } = React.useContext(SearchContext);
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Translate translationKey="artistFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
|
||||||
{artistData?.length ? (
|
|
||||||
<CardGridCustom
|
|
||||||
content={artistData
|
|
||||||
.slice(0, props.maxItems ?? artistData.length)
|
|
||||||
.map((artistData) => ({
|
|
||||||
image: API.getArtistIllustration(artistData.id),
|
|
||||||
name: artistData.name,
|
|
||||||
id: artistData.id,
|
|
||||||
onPress: () => {
|
|
||||||
API.createSearchHistoryEntry(artistData.name, 'artist');
|
|
||||||
navigation.navigate('Artist', { artistId: artistData.id });
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
cardComponent={ArtistCard}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Translate translationKey="errNoResults" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GenreSearchComponent = (props: ItemSearchComponentProps) => {
|
|
||||||
const { genreData } = React.useContext(SearchContext);
|
|
||||||
const navigation = useNavigation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Translate translationKey="genreFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
|
||||||
{genreData?.length ? (
|
|
||||||
<CardGridCustom
|
|
||||||
content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
|
|
||||||
image: API.getGenreIllustration(g.id),
|
|
||||||
name: g.name,
|
|
||||||
id: g.id,
|
|
||||||
onPress: () => {
|
|
||||||
API.createSearchHistoryEntry(g.name, 'genre');
|
|
||||||
navigation.navigate('Genre', { genreId: g.id });
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
cardComponent={GenreCard}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Translate translationKey="errNoResults" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FavoritesComponent = () => {
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const favoritesQuery = useQuery(API.getLikedSongs());
|
|
||||||
|
|
||||||
if (favoritesQuery.isError) {
|
|
||||||
navigation.navigate('Error');
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
if (!favoritesQuery.data) {
|
|
||||||
return <LoadingView />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView>
|
|
||||||
<Translate translationKey="songsFilter" fontSize="xl" fontWeight="bold" mt={4} />
|
|
||||||
<Box>
|
|
||||||
{favoritesQuery.data?.map((songData) => (
|
|
||||||
<FavSongRow
|
|
||||||
key={songData.id}
|
|
||||||
song={songData.song}
|
|
||||||
addedDate={songData.addedDate}
|
|
||||||
onPress={() => {
|
|
||||||
API.createSearchHistoryEntry(songData.song.name, 'song'); //todo
|
|
||||||
navigation.navigate('Play', { songId: songData.song!.id });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AllComponent = () => {
|
|
||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
|
||||||
const isMobileView = screenSize == 'small';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView>
|
|
||||||
<Flex
|
|
||||||
flexWrap="wrap"
|
|
||||||
direction={isMobileView ? 'column' : 'row'}
|
|
||||||
justifyContent={['flex-start']}
|
|
||||||
mt={4}
|
|
||||||
>
|
|
||||||
<Column w={isMobileView ? '100%' : '50%'}>
|
|
||||||
<Box minH={isMobileView ? 100 : 200}>
|
|
||||||
<ArtistSearchComponent maxItems={6} />
|
|
||||||
</Box>
|
|
||||||
<Box minH={isMobileView ? 100 : 200}>
|
|
||||||
<GenreSearchComponent maxItems={6} />
|
|
||||||
</Box>
|
|
||||||
</Column>
|
|
||||||
<Box w={isMobileView ? '100%' : '50%'}>
|
|
||||||
<SongsSearchComponent maxRows={9} />
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FilterSwitch = () => {
|
|
||||||
const { filter } = React.useContext(SearchContext);
|
|
||||||
const [currentFilter, setCurrentFilter] = React.useState(filter);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setCurrentFilter(filter);
|
|
||||||
}, [filter]);
|
|
||||||
|
|
||||||
switch (currentFilter) {
|
|
||||||
case 'all':
|
|
||||||
return <AllComponent />;
|
|
||||||
case 'song':
|
|
||||||
return <SongsSearchComponent />;
|
|
||||||
case 'artist':
|
|
||||||
return <ArtistSearchComponent />;
|
|
||||||
case 'genre':
|
|
||||||
return <GenreSearchComponent />;
|
|
||||||
case 'favorites':
|
|
||||||
return <FavoritesComponent />;
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<Translate translationKey="unknownError" format={(e) => `${e}: ${currentFilter}`} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SearchResultComponent = () => {
|
|
||||||
const { stringQuery } = React.useContext(SearchContext);
|
|
||||||
const { filter } = React.useContext(SearchContext);
|
|
||||||
const shouldOutput = !!stringQuery.trim() || filter == 'favorites';
|
|
||||||
|
|
||||||
return shouldOutput ? (
|
|
||||||
<Box p={5}>
|
|
||||||
<FilterSwitch />
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<HomeSearchComponent />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,8 +6,8 @@ import ButtonBase from '../UI/ButtonBase';
|
|||||||
import { AddSquare, CloseCircle, SearchNormal1 } from 'iconsax-react-native';
|
import { AddSquare, CloseCircle, SearchNormal1 } from 'iconsax-react-native';
|
||||||
import { useQuery } from '../../Queries';
|
import { useQuery } from '../../Queries';
|
||||||
import API from '../../API';
|
import API from '../../API';
|
||||||
import Genre from '../../models/Genre';
|
|
||||||
import { translate } from '../../i18n/i18n';
|
import { translate } from '../../i18n/i18n';
|
||||||
|
import { searchProps } from '../../views/V2/SearchView';
|
||||||
|
|
||||||
type ArtistChipProps = {
|
type ArtistChipProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,9 +29,9 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.selected ? (
|
{props.selected ? (
|
||||||
<CloseCircle size="32" color={'#ED4A51'} />
|
<CloseCircle size="24" color={'#ED4A51'} />
|
||||||
) : (
|
) : (
|
||||||
<AddSquare size="32" color={'#6075F9'} />
|
<AddSquare size="24" color={'#6075F9'} />
|
||||||
)}
|
)}
|
||||||
<Text>{props.name}</Text>
|
<Text>{props.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -40,15 +40,24 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchBarComponent = () => {
|
const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => void }) => {
|
||||||
const [query, setQuery] = React.useState('');
|
const [query, setQuery] = React.useState('');
|
||||||
const [genre, setGenre] = React.useState({} as Genre | undefined);
|
const [genre, setGenre] = React.useState('');
|
||||||
const [artist, setArtist] = React.useState('');
|
const [artist, setArtist] = React.useState('');
|
||||||
const artistsQuery = useQuery(API.getAllArtists());
|
const artistsQuery = useQuery(API.getAllArtists());
|
||||||
const genresQuery = useQuery(API.getAllGenres());
|
const genresQuery = useQuery(API.getAllGenres());
|
||||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||||
const isMobileView = screenSize == 'small';
|
const isMobileView = screenSize == 'small';
|
||||||
|
|
||||||
|
const handleValidate = () => {
|
||||||
|
const searchData = {
|
||||||
|
query: query,
|
||||||
|
artist: artistsQuery.data?.find((a) => a.name === artist)?.id ?? undefined,
|
||||||
|
genre: genresQuery.data?.find((g) => g.name === genre)?.id ?? undefined,
|
||||||
|
};
|
||||||
|
props.onValidate(searchData);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View
|
<View
|
||||||
@@ -57,7 +66,7 @@ const SearchBarComponent = () => {
|
|||||||
borderBottomColor: '#9E9E9E',
|
borderBottomColor: '#9E9E9E',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: isMobileView ? 'column' : 'row',
|
flexDirection: isMobileView ? 'column' : 'row',
|
||||||
alignItems: 'center',
|
maxWidth: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
margin: 5,
|
margin: 5,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
@@ -69,10 +78,11 @@ const SearchBarComponent = () => {
|
|||||||
flexGrow: 0,
|
flexGrow: 0,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'nowrap',
|
||||||
|
maxWidth: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{artist && (
|
{!!artist && (
|
||||||
<ArtistChipComponent
|
<ArtistChipComponent
|
||||||
onPress={() => setArtist('')}
|
onPress={() => setArtist('')}
|
||||||
name={artist}
|
name={artist}
|
||||||
@@ -82,36 +92,27 @@ const SearchBarComponent = () => {
|
|||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexGrow: 1,
|
|
||||||
width: '100%',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View style={{ flex: 1 }}>
|
||||||
style={{
|
|
||||||
flexGrow: 1,
|
|
||||||
flexShrink: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
variant={'unstyled'}
|
variant={'unstyled'}
|
||||||
placeholder={translate('searchBarPlaceholder')}
|
placeholder={translate('searchBarPlaceholder')}
|
||||||
style={{ width: '100%', height: 30 }}
|
style={{ height: 30 }}
|
||||||
onChangeText={(value) => setQuery(value)}
|
onChangeText={(value) => setQuery(value)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
type="menu"
|
type="menu"
|
||||||
icon={SearchNormal1}
|
icon={SearchNormal1}
|
||||||
style={{
|
style={{}}
|
||||||
flexShrink: 0,
|
onPress={handleValidate}
|
||||||
flexGrow: 0,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -146,6 +147,13 @@ const SearchBarComponent = () => {
|
|||||||
key={index}
|
key={index}
|
||||||
name={artist.name}
|
name={artist.name}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
props.onValidate({
|
||||||
|
artist: artist.id,
|
||||||
|
genre:
|
||||||
|
genresQuery.data?.find((a) => a.name === genre)
|
||||||
|
?.id ?? undefined,
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
setArtist(artist.name);
|
setArtist(artist.name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -154,15 +162,20 @@ const SearchBarComponent = () => {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View>
|
<View>
|
||||||
<Select
|
<Select
|
||||||
selectedValue={genre?.name}
|
selectedValue={genre}
|
||||||
placeholder={translate('genreFilter')}
|
placeholder={translate('genreFilter')}
|
||||||
accessibilityLabel="Genre"
|
accessibilityLabel="Genre"
|
||||||
onValueChange={(itemValue) => {
|
onValueChange={(itemValue) => {
|
||||||
setGenre(
|
setGenre(itemValue);
|
||||||
genresQuery.data?.find((genre) => {
|
props.onValidate({
|
||||||
genre.name == itemValue;
|
artist:
|
||||||
})
|
artistsQuery.data?.find((a) => a.name === artist)?.id ??
|
||||||
);
|
undefined,
|
||||||
|
genre:
|
||||||
|
genresQuery.data?.find((g) => g.name === itemValue)?.id ??
|
||||||
|
undefined,
|
||||||
|
query: query,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select.Item label={translate('emptySelection')} value="" />
|
<Select.Item label={translate('emptySelection')} value="" />
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ export const en = {
|
|||||||
leaderBoardHeading: 'These are the best players',
|
leaderBoardHeading: 'These are the best players',
|
||||||
leaderBoardHeadingFull:
|
leaderBoardHeadingFull:
|
||||||
'The players having the best scores, thanks to their exceptional accuracy, are highlighted here.',
|
'The players having the best scores, thanks to their exceptional accuracy, are highlighted here.',
|
||||||
emptySelection: 'None,',
|
emptySelection: 'None',
|
||||||
gamesPlayed: 'Games Played',
|
gamesPlayed: 'Games Played',
|
||||||
metronome: 'Metronome',
|
metronome: 'Metronome',
|
||||||
loading: 'Loading... Please Wait',
|
loading: 'Loading... Please Wait',
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import SearchBar from '../components/SearchBar';
|
|
||||||
import Artist from '../models/Artist';
|
|
||||||
import Song from '../models/Song';
|
|
||||||
import Genre from '../models/Genre';
|
|
||||||
import API from '../API';
|
|
||||||
import { useQuery } from '../Queries';
|
|
||||||
import { SearchResultComponent } from '../components/SearchResult';
|
|
||||||
import { SafeAreaView } from 'react-native';
|
|
||||||
import { Filter } from '../components/SearchBar';
|
|
||||||
import { ScrollView } from 'native-base';
|
|
||||||
import LikedSong from '../models/LikedSong';
|
|
||||||
|
|
||||||
interface SearchContextType {
|
|
||||||
filter: 'artist' | 'song' | 'genre' | 'all' | 'favorites';
|
|
||||||
updateFilter: (newData: 'artist' | 'song' | 'genre' | 'all' | 'favorites') => void;
|
|
||||||
stringQuery: string;
|
|
||||||
updateStringQuery: (newData: string) => void;
|
|
||||||
songData: Song[];
|
|
||||||
artistData: Artist[];
|
|
||||||
genreData: Genre[];
|
|
||||||
favoriteData: LikedSong[];
|
|
||||||
isLoadingSong: boolean;
|
|
||||||
isLoadingArtist: boolean;
|
|
||||||
isLoadingGenre: boolean;
|
|
||||||
isLoadingFavorite: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchContext = React.createContext<SearchContextType>({
|
|
||||||
filter: 'all',
|
|
||||||
updateFilter: () => {},
|
|
||||||
stringQuery: '',
|
|
||||||
updateStringQuery: () => {},
|
|
||||||
songData: [],
|
|
||||||
artistData: [],
|
|
||||||
genreData: [],
|
|
||||||
favoriteData: [],
|
|
||||||
isLoadingSong: false,
|
|
||||||
isLoadingArtist: false,
|
|
||||||
isLoadingGenre: false,
|
|
||||||
isLoadingFavorite: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
type SearchViewProps = {
|
|
||||||
query?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SearchView = (props: SearchViewProps) => {
|
|
||||||
const [filter, setFilter] = useState<Filter>('all');
|
|
||||||
const [stringQuery, setStringQuery] = useState<string>(props?.query ?? '');
|
|
||||||
|
|
||||||
const { isLoading: isLoadingSong, data: songData = [] } = useQuery(
|
|
||||||
API.searchSongs(stringQuery),
|
|
||||||
{ enabled: !!stringQuery }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery(
|
|
||||||
API.searchArtists(stringQuery),
|
|
||||||
{ enabled: !!stringQuery }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery(
|
|
||||||
API.searchGenres(stringQuery),
|
|
||||||
{ enabled: !!stringQuery }
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isLoading: isLoadingFavorite, data: favoriteData = [] } = useQuery(
|
|
||||||
API.getLikedSongs(),
|
|
||||||
{ enabled: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateFilter = (newData: Filter) => {
|
|
||||||
// called when the filter is changed
|
|
||||||
setFilter(newData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateStringQuery = (newData: string) => {
|
|
||||||
// called when the stringQuery is updated
|
|
||||||
setStringQuery(newData);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollView>
|
|
||||||
<SafeAreaView>
|
|
||||||
<SearchContext.Provider
|
|
||||||
value={{
|
|
||||||
filter,
|
|
||||||
stringQuery,
|
|
||||||
songData,
|
|
||||||
artistData,
|
|
||||||
genreData,
|
|
||||||
favoriteData,
|
|
||||||
isLoadingSong,
|
|
||||||
isLoadingArtist,
|
|
||||||
isLoadingGenre,
|
|
||||||
isLoadingFavorite,
|
|
||||||
updateFilter,
|
|
||||||
updateStringQuery,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SearchBar />
|
|
||||||
<SearchResultComponent />
|
|
||||||
</SearchContext.Provider>
|
|
||||||
</SafeAreaView>
|
|
||||||
</ScrollView>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchView;
|
|
||||||
@@ -1,12 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { useQuery } from '../../Queries';
|
||||||
import SearchBarComponent from '../../components/V2/SearchBar';
|
import SearchBarComponent from '../../components/V2/SearchBar';
|
||||||
import SearchHistory from '../../components/V2/SearchHistory';
|
import SearchHistory from '../../components/V2/SearchHistory';
|
||||||
import { View } from 'react-native';
|
import API from '../../API';
|
||||||
|
import LoadingComponent from '../../components/Loading';
|
||||||
|
import MusicListCC from '../../components/UI/MusicList';
|
||||||
|
|
||||||
|
export type searchProps = {
|
||||||
|
artist: number | undefined;
|
||||||
|
genre: number | undefined;
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
|
|
||||||
const SearchView = () => {
|
const SearchView = () => {
|
||||||
|
const artistsQuery = useQuery(API.getAllArtists());
|
||||||
|
const [searchQuery, setSearchQuery] = React.useState({} as searchProps);
|
||||||
|
const rawResult = useQuery(API.searchSongs(searchQuery, ['artist']), {
|
||||||
|
enabled: !!searchQuery.query || !!searchQuery.artist || !!searchQuery.genre,
|
||||||
|
onSuccess() {
|
||||||
|
const artist =
|
||||||
|
artistsQuery?.data?.find(({ id }) => id == searchQuery.artist)?.name ??
|
||||||
|
'unknown artist';
|
||||||
|
searchQuery.query ? API.createSearchHistoryEntry(searchQuery.query, 'song') : null;
|
||||||
|
if (artist != 'unknown artist') API.createSearchHistoryEntry(artist, 'artist');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (artistsQuery.isLoading) {
|
||||||
|
return <LoadingComponent />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ display: 'flex', gap: 50 }}>
|
<View style={{ display: 'flex', gap: 20 }}>
|
||||||
<SearchBarComponent />
|
<SearchBarComponent onValidate={(query) => setSearchQuery(query)} />
|
||||||
<SearchHistory />
|
{rawResult.isSuccess ? (
|
||||||
|
<MusicListCC
|
||||||
|
musics={rawResult.data}
|
||||||
|
isFetching={rawResult.isFetching}
|
||||||
|
refetch={rawResult.refetch}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SearchHistory />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user