Merge pull request #336 from Chroma-Case/feat/adc/search-view-v2
implémentation de la search bar V2
This commit is contained in:
@@ -314,16 +314,10 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully added score'})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Patch('me/score/:score')
|
||||
addScore(
|
||||
@Request() req: any,
|
||||
@Param('id') score: number,
|
||||
) {
|
||||
return this.usersService.addScore(
|
||||
+req.user.id,
|
||||
score,
|
||||
);
|
||||
@ApiOkResponse({ description: "Successfully added score" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Patch("me/score/:score")
|
||||
addScore(@Request() req: any, @Param("id") score: number) {
|
||||
return this.usersService.addScore(+req.user.id, score);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { ScoresService } from './scores.service';
|
||||
import { User } from '@prisma/client';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@ApiTags('scores')
|
||||
@Controller('scores')
|
||||
@ApiTags("scores")
|
||||
@Controller("scores")
|
||||
export class ScoresController {
|
||||
constructor(private readonly scoresService: ScoresService) {}
|
||||
|
||||
@ApiOkResponse({ description: 'Successfully sent the Top 20 players'})
|
||||
@Get('top/20')
|
||||
@ApiOkResponse({ description: "Successfully sent the Top 20 players" })
|
||||
@Get("top/20")
|
||||
getTopTwenty(): Promise<User[]> {
|
||||
return this.scoresService.topTwenty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
front/API.ts
61
front/API.ts
@@ -400,6 +400,19 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
public static getAllGenres(): Query<Genre[]> {
|
||||
return {
|
||||
key: ['genres'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/genre',
|
||||
},
|
||||
{ handler: PlageHandler(GenreHandler) }
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive a song's musicXML partition
|
||||
* @param songId the id to find the song
|
||||
@@ -433,6 +446,19 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
public static getAllArtists(): Query<Artist[]> {
|
||||
return {
|
||||
key: ['artists'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/artist`,
|
||||
},
|
||||
{ handler: PlageHandler(ArtistHandler) }
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive a song's chapters
|
||||
* @param songId the id to find the song
|
||||
@@ -716,24 +742,6 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
public static getSongCursorInfos(songId: number): Query<SongCursorInfos> {
|
||||
return {
|
||||
key: ['cursorInfos', songId],
|
||||
exec: () => {
|
||||
return API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/assets/cursors`,
|
||||
},
|
||||
{ handler: SongCursorInfosHandler }
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
|
||||
public static async updateUserTotalScore(score: number): Promise<void> {
|
||||
await API.fetch({
|
||||
route: `/auth/me/score/${score}`,
|
||||
@@ -754,4 +762,21 @@ export default class API {
|
||||
),
|
||||
};
|
||||
}
|
||||
public static getSongCursorInfos(songId: number): Query<SongCursorInfos> {
|
||||
return {
|
||||
key: ['cursorInfos', songId],
|
||||
exec: () => {
|
||||
return API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/assets/cursors`,
|
||||
},
|
||||
{ handler: SongCursorInfosHandler }
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/
|
||||
import { RootState, useSelector } from './state/Store';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Translate, translate } from './i18n/i18n';
|
||||
import SearchView from './views/SearchView';
|
||||
import SearchView from './views/V2/SearchView';
|
||||
import SettingsTab from './views/settings/SettingsView';
|
||||
import { useQuery } from './Queries';
|
||||
import API, { APIError } from './API';
|
||||
@@ -49,11 +49,6 @@ const protectedRoutes = () =>
|
||||
options: { headerShown: false },
|
||||
link: '/music',
|
||||
},
|
||||
HomeNew: {
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false },
|
||||
link: '/V2',
|
||||
},
|
||||
Play: {
|
||||
component: PlayView,
|
||||
options: { headerShown: false, title: translate('play') },
|
||||
|
||||
@@ -108,7 +108,7 @@ const ScoreModal = (props: ScoreModalProps) => {
|
||||
onPress={() =>
|
||||
navigation.canGoBack()
|
||||
? navigation.goBack()
|
||||
: navigation.navigate('HomeNew', {})
|
||||
: navigation.navigate('Home', {})
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
@@ -10,7 +10,7 @@ import ScaffoldMobileCC from './ScaffoldMobileCC';
|
||||
import { useAssets } from 'expo-asset';
|
||||
|
||||
const menu = [
|
||||
{ type: 'main', title: 'menuDiscovery', icon: Discover, link: 'HomeNew' },
|
||||
{ type: 'main', title: 'menuDiscovery', icon: Discover, link: 'Home' },
|
||||
{ type: 'main', title: 'menuProfile', icon: User, link: 'User' },
|
||||
{ type: 'main', title: 'menuMusic', icon: Music, link: 'Music' },
|
||||
{ type: 'main', title: 'menuSearch', icon: SearchNormal1, link: 'Search' },
|
||||
|
||||
179
front/components/V2/SearchBar.tsx
Normal file
179
front/components/V2/SearchBar.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
import { Button, Text, Select, useBreakpointValue } from 'native-base';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { Input } from 'native-base';
|
||||
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';
|
||||
|
||||
type ArtistChipProps = {
|
||||
name: string;
|
||||
selected?: boolean;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const ArtistChipComponent = (props: ArtistChipProps) => {
|
||||
return (
|
||||
<View>
|
||||
<Button variant={'ghost'} onPress={props.onPress}>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{props.selected ? (
|
||||
<CloseCircle size="32" color={'#ED4A51'} />
|
||||
) : (
|
||||
<AddSquare size="32" color={'#6075F9'} />
|
||||
)}
|
||||
<Text>{props.name}</Text>
|
||||
</View>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchBarComponent = () => {
|
||||
const [query, setQuery] = React.useState('');
|
||||
const [genre, setGenre] = React.useState({} as Genre | undefined);
|
||||
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';
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#9E9E9E',
|
||||
display: 'flex',
|
||||
flexDirection: isMobileView ? 'column' : 'row',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
margin: 5,
|
||||
padding: 16,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{artist && (
|
||||
<ArtistChipComponent
|
||||
onPress={() => setArtist('')}
|
||||
name={artist}
|
||||
selected={true}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={query}
|
||||
variant={'unstyled'}
|
||||
placeholder={translate('searchBarPlaceholder')}
|
||||
style={{ width: '100%', height: 30 }}
|
||||
onChangeText={(value) => setQuery(value)}
|
||||
/>
|
||||
</View>
|
||||
<ButtonBase
|
||||
type="menu"
|
||||
icon={SearchNormal1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
flexGrow: 0,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
style={{
|
||||
paddingBottom: 10,
|
||||
maxWidth: 1200,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{!artist &&
|
||||
artistsQuery.data?.map((artist, index) => (
|
||||
<ArtistChipComponent
|
||||
key={index}
|
||||
name={artist.name}
|
||||
onPress={() => {
|
||||
setArtist(artist.name);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View>
|
||||
<Select
|
||||
selectedValue={genre?.name}
|
||||
placeholder={translate('genreFilter')}
|
||||
accessibilityLabel="Genre"
|
||||
onValueChange={(itemValue) => {
|
||||
setGenre(
|
||||
genresQuery.data?.find((genre) => {
|
||||
genre.name == itemValue;
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Select.Item label={translate('emptySelection')} value="" />
|
||||
{genresQuery.data?.map((data, index) => (
|
||||
<Select.Item key={index} label={data.name} value={data.name} />
|
||||
))}
|
||||
</Select>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBarComponent;
|
||||
@@ -84,6 +84,7 @@ export const en = {
|
||||
songsFilter: 'Songs',
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favorites',
|
||||
searchBarPlaceholder: 'What are you looking for ?',
|
||||
|
||||
// profile page
|
||||
user: 'Profile',
|
||||
@@ -306,6 +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,',
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
@@ -395,6 +397,7 @@ export const fr: typeof en = {
|
||||
songsFilter: 'Morceaux',
|
||||
genreFilter: 'Genres',
|
||||
favoriteFilter: 'Favoris',
|
||||
searchBarPlaceholder: 'Que recherchez vous ?',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Difficulté',
|
||||
@@ -617,6 +620,7 @@ export const fr: typeof en = {
|
||||
leaderBoardHeading: 'Voici les meilleurs joueurs',
|
||||
leaderBoardHeadingFull:
|
||||
'Les joueurs présentant les meilleurs scores, grâce à leur précision exceptionnelle, sont mis en lumière ici.',
|
||||
emptySelection: 'Aucun',
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
@@ -720,6 +724,7 @@ export const sp: typeof en = {
|
||||
songsFilter: 'canciones',
|
||||
genreFilter: 'géneros',
|
||||
favoriteFilter: 'Favorites',
|
||||
searchBarPlaceholder: 'Qué estás buscando ?',
|
||||
|
||||
// Difficulty settings
|
||||
diffBtn: 'Dificultad',
|
||||
@@ -934,4 +939,5 @@ export const sp: typeof en = {
|
||||
leaderBoardHeading: 'Estos son los mejores jugadores.',
|
||||
leaderBoardHeadingFull:
|
||||
'Aquí se destacan los jugadores que tienen las mejores puntuaciones, gracias a su precisión excepcional.',
|
||||
emptySelection: 'Nada',
|
||||
};
|
||||
|
||||
@@ -93,7 +93,7 @@ const HomeView = (props: RouteProps<{}>) => {
|
||||
label={'V2'}
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
onPress={() => navigation.navigate('HomeNew', {})}
|
||||
onPress={() => navigation.navigate('Home', {})}
|
||||
/>
|
||||
</HStack>
|
||||
<Box style={{ width: '100%' }}>
|
||||
@@ -126,9 +126,7 @@ const HomeView = (props: RouteProps<{}>) => {
|
||||
size="xs"
|
||||
colorScheme="primary"
|
||||
label={query}
|
||||
onPress={() =>
|
||||
navigation.navigate('Search', { query: query })
|
||||
}
|
||||
onPress={() => navigation.navigate('Search', {})}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
15
front/views/V2/SearchView.tsx
Normal file
15
front/views/V2/SearchView.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import ScaffoldCC from '../../components/UI/ScaffoldCC';
|
||||
import SearchBarComponent from '../../components/V2/SearchBar';
|
||||
import { RouteProps } from '../../Navigation';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const SearchView = (props: RouteProps<{}>) => {
|
||||
return (
|
||||
<ScaffoldCC routeName={props.route.name}>
|
||||
<SearchBarComponent />
|
||||
</ScaffoldCC>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchView;
|
||||
Reference in New Issue
Block a user