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
|
||||
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 { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
@@ -13,4 +13,10 @@ export class ScoresController {
|
||||
getTopTwenty(): Promise<User[]> {
|
||||
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 Album from './models/Album';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre, { GenreHandler } from './models/Genre';
|
||||
@@ -24,6 +23,7 @@ import * as yup from 'yup';
|
||||
import { base64ToBlob } from './utils/base64ToBlob';
|
||||
import { ImagePickerAsset } from 'expo-image-picker';
|
||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||
import { searchProps } from './views/V2/SearchView';
|
||||
|
||||
type AuthenticationInput = { username: string; password: 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
|
||||
* @param lessonId the id to find the lesson
|
||||
@@ -780,6 +702,29 @@ export default class API {
|
||||
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 {
|
||||
return `${API.baseUrl}/song/${songId}/assets/melody`;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ const Graph = ({ songId, since }: GraphProps) => {
|
||||
|
||||
const ScoreGraph = () => {
|
||||
const layout = useWindowDimensions();
|
||||
const songs = useQuery(API.getAllSongs);
|
||||
const songs = useQuery(API.getAllSongs());
|
||||
const rangeOptions = [
|
||||
{ label: '3 derniers jours', value: '3days' },
|
||||
{ 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 { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import Genre from '../../models/Genre';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { searchProps } from '../../views/V2/SearchView';
|
||||
|
||||
type ArtistChipProps = {
|
||||
name: string;
|
||||
@@ -29,9 +29,9 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
</View>
|
||||
@@ -40,15 +40,24 @@ const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SearchBarComponent = () => {
|
||||
const SearchBarComponent = (props: { onValidate: (searchData: searchProps) => void }) => {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [genre, setGenre] = React.useState({} as Genre | undefined);
|
||||
const [genre, setGenre] = React.useState('');
|
||||
const [artist, setArtist] = React.useState('');
|
||||
const artistsQuery = useQuery(API.getAllArtists());
|
||||
const genresQuery = useQuery(API.getAllGenres());
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
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 (
|
||||
<View>
|
||||
<View
|
||||
@@ -57,7 +66,7 @@ const SearchBarComponent = () => {
|
||||
borderBottomColor: '#9E9E9E',
|
||||
display: 'flex',
|
||||
flexDirection: isMobileView ? 'column' : 'row',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
margin: 5,
|
||||
padding: 16,
|
||||
@@ -69,10 +78,11 @@ const SearchBarComponent = () => {
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexWrap: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{artist && (
|
||||
{!!artist && (
|
||||
<ArtistChipComponent
|
||||
onPress={() => setArtist('')}
|
||||
name={artist}
|
||||
@@ -82,36 +92,27 @@ const SearchBarComponent = () => {
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
variant={'unstyled'}
|
||||
placeholder={translate('searchBarPlaceholder')}
|
||||
style={{ width: '100%', height: 30 }}
|
||||
style={{ height: 30 }}
|
||||
onChangeText={(value) => setQuery(value)}
|
||||
/>
|
||||
</View>
|
||||
<ButtonBase
|
||||
type="menu"
|
||||
icon={SearchNormal1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
style={{}}
|
||||
onPress={handleValidate}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -146,6 +147,13 @@ const SearchBarComponent = () => {
|
||||
key={index}
|
||||
name={artist.name}
|
||||
onPress={() => {
|
||||
props.onValidate({
|
||||
artist: artist.id,
|
||||
genre:
|
||||
genresQuery.data?.find((a) => a.name === genre)
|
||||
?.id ?? undefined,
|
||||
query: query,
|
||||
});
|
||||
setArtist(artist.name);
|
||||
}}
|
||||
/>
|
||||
@@ -154,15 +162,20 @@ const SearchBarComponent = () => {
|
||||
</ScrollView>
|
||||
<View>
|
||||
<Select
|
||||
selectedValue={genre?.name}
|
||||
selectedValue={genre}
|
||||
placeholder={translate('genreFilter')}
|
||||
accessibilityLabel="Genre"
|
||||
onValueChange={(itemValue) => {
|
||||
setGenre(
|
||||
genresQuery.data?.find((genre) => {
|
||||
genre.name == itemValue;
|
||||
})
|
||||
);
|
||||
setGenre(itemValue);
|
||||
props.onValidate({
|
||||
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="" />
|
||||
|
||||
@@ -307,7 +307,7 @@ export const en = {
|
||||
leaderBoardHeading: 'These are the best players',
|
||||
leaderBoardHeadingFull:
|
||||
'The players having the best scores, thanks to their exceptional accuracy, are highlighted here.',
|
||||
emptySelection: 'None,',
|
||||
emptySelection: 'None',
|
||||
gamesPlayed: 'Games Played',
|
||||
metronome: 'Metronome',
|
||||
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 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 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 (
|
||||
<View style={{ display: 'flex', gap: 50 }}>
|
||||
<SearchBarComponent />
|
||||
<SearchHistory />
|
||||
<View style={{ display: 'flex', gap: 20 }}>
|
||||
<SearchBarComponent onValidate={(query) => setSearchQuery(query)} />
|
||||
{rawResult.isSuccess ? (
|
||||
<MusicListCC
|
||||
musics={rawResult.data}
|
||||
isFetching={rawResult.isFetching}
|
||||
refetch={rawResult.refetch}
|
||||
/>
|
||||
) : (
|
||||
<SearchHistory />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user