From c0bc6112685ebac8ad1adae62cabc2b0a39fbafd Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 29 Nov 2023 21:45:53 +0100 Subject: [PATCH] Use includes on liked, music, score, search and fav pages --- back/src/auth/auth.controller.ts | 205 +++++++++++++++--------------- back/src/users/users.service.ts | 27 ++-- front/API.ts | 16 ++- front/Queries.ts | 9 +- front/components/FavSongRow.tsx | 19 +-- front/components/SearchResult.tsx | 59 ++------- front/models/LikedSong.ts | 25 +--- front/views/MusicView.tsx | 19 ++- front/views/ScoreView.tsx | 27 +--- 9 files changed, 169 insertions(+), 237 deletions(-) diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index 4e613e7..ffd18a0 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -21,12 +21,12 @@ import { Response, Query, Param, -} from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { JwtAuthGuard } from './jwt-auth.guard'; -import { LocalAuthGuard } from './local-auth.guard'; -import { RegisterDto } from './dto/register.dto'; -import { UsersService } from 'src/users/users.service'; +} from "@nestjs/common"; +import { AuthService } from "./auth.service"; +import { JwtAuthGuard } from "./jwt-auth.guard"; +import { LocalAuthGuard } from "./local-auth.guard"; +import { RegisterDto } from "./dto/register.dto"; +import { UsersService } from "src/users/users.service"; import { ApiBadRequestResponse, ApiBearerAuth, @@ -36,39 +36,41 @@ import { ApiOperation, ApiTags, ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { User } from '../models/user'; -import { JwtToken } from './models/jwt'; -import { LoginDto } from './dto/login.dto'; -import { Profile } from './dto/profile.dto'; -import { Setting } from 'src/models/setting'; -import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto'; -import { SettingsService } from 'src/settings/settings.service'; -import { AuthGuard } from '@nestjs/passport'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { writeFile } from 'fs'; -import { PasswordResetDto } from './dto/password_reset.dto '; +} from "@nestjs/swagger"; +import { User } from "../models/user"; +import { JwtToken } from "./models/jwt"; +import { LoginDto } from "./dto/login.dto"; +import { Profile } from "./dto/profile.dto"; +import { Setting } from "src/models/setting"; +import { UpdateSettingDto } from "src/settings/dto/update-setting.dto"; +import { SettingsService } from "src/settings/settings.service"; +import { AuthGuard } from "@nestjs/passport"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { writeFile } from "fs"; +import { PasswordResetDto } from "./dto/password_reset.dto "; +import { mapInclude } from "src/utils/include"; +import { SongController } from "src/song/song.controller"; -@ApiTags('auth') -@Controller('auth') +@ApiTags("auth") +@Controller("auth") export class AuthController { constructor( private authService: AuthService, private usersService: UsersService, private settingsService: SettingsService, - ) {} + ) { } - @Get('login/google') - @UseGuards(AuthGuard('google')) - @ApiOperation({ description: 'Redirect to google login page' }) - googleLogin() {} + @Get("login/google") + @UseGuards(AuthGuard("google")) + @ApiOperation({ description: "Redirect to google login page" }) + googleLogin() { } - @Get('logged/google') + @Get("logged/google") @ApiOperation({ description: - 'Redirect to the front page after connecting to the google account', + "Redirect to the front page after connecting to the google account", }) - @UseGuards(AuthGuard('google')) + @UseGuards(AuthGuard("google")) async googleLoginCallbakc(@Req() req: any) { let user = await this.usersService.user({ googleID: req.user.googleID }); if (!user) { @@ -78,13 +80,13 @@ export class AuthController { return this.authService.login(user); } - @Post('register') - @ApiOperation({ description: 'Register a new user' }) - @ApiConflictResponse({ description: 'Username or email already taken' }) + @Post("register") + @ApiOperation({ description: "Register a new user" }) + @ApiConflictResponse({ description: "Username or email already taken" }) @ApiOkResponse({ - description: 'Successfully registered, email sent to verify', + description: "Successfully registered, email sent to verify", }) - @ApiBadRequestResponse({ description: 'Invalid data or database error' }) + @ApiBadRequestResponse({ description: "Invalid data or database error" }) async register(@Body() registerDto: RegisterDto): Promise { try { const user = await this.usersService.createUser(registerDto); @@ -92,73 +94,73 @@ export class AuthController { await this.authService.sendVerifyMail(user); } catch (e) { // check if the error is a duplicate key error - if (e.code === 'P2002') { - throw new ConflictException('Username or email already taken'); + if (e.code === "P2002") { + throw new ConflictException("Username or email already taken"); } console.error(e); throw new BadRequestException(); } } - @Put('verify') + @Put("verify") @HttpCode(200) @UseGuards(JwtAuthGuard) - @ApiOperation({ description: 'Verify the email of the user' }) - @ApiOkResponse({ description: 'Successfully verified' }) - @ApiBadRequestResponse({ description: 'Invalid or expired token' }) + @ApiOperation({ description: "Verify the email of the user" }) + @ApiOkResponse({ description: "Successfully verified" }) + @ApiBadRequestResponse({ description: "Invalid or expired token" }) async verify( @Request() req: any, - @Query('token') token: string, + @Query("token") token: string, ): Promise { if (await this.authService.verifyMail(req.user.id, token)) return; - throw new BadRequestException('Invalid token. Expired or invalid.'); + throw new BadRequestException("Invalid token. Expired or invalid."); } - @Put('reverify') + @Put("reverify") @UseGuards(JwtAuthGuard) @HttpCode(200) - @ApiOperation({ description: 'Resend the verification email' }) + @ApiOperation({ description: "Resend the verification email" }) async reverify(@Request() req: any): Promise { const user = await this.usersService.user({ id: req.user.id }); - if (!user) throw new BadRequestException('Invalid user'); + if (!user) throw new BadRequestException("Invalid user"); await this.authService.sendVerifyMail(user); } @HttpCode(200) - @Put('password-reset') + @Put("password-reset") async password_reset( @Body() resetDto: PasswordResetDto, - @Query('token') token: string, + @Query("token") token: string, ): Promise { if (await this.authService.changePassword(resetDto.password, token)) return; - throw new BadRequestException('Invalid token. Expired or invalid.'); + throw new BadRequestException("Invalid token. Expired or invalid."); } @HttpCode(200) - @Put('forgot-password') - async forgot_password(@Query('email') email: string): Promise { + @Put("forgot-password") + async forgot_password(@Query("email") email: string): Promise { console.log(email); const user = await this.usersService.user({ email: email }); - if (!user) throw new BadRequestException('Invalid user'); + if (!user) throw new BadRequestException("Invalid user"); await this.authService.sendPasswordResetMail(user); } - @Post('login') + @Post("login") @ApiBody({ type: LoginDto }) @HttpCode(200) @UseGuards(LocalAuthGuard) @ApiBody({ type: LoginDto }) - @ApiOperation({ description: 'Login with username and password' }) - @ApiOkResponse({ description: 'Successfully logged in', type: JwtToken }) - @ApiUnauthorizedResponse({ description: 'Invalid credentials' }) + @ApiOperation({ description: "Login with username and password" }) + @ApiOkResponse({ description: "Successfully logged in", type: JwtToken }) + @ApiUnauthorizedResponse({ description: "Invalid credentials" }) async login(@Request() req: any): Promise { return this.authService.login(req.user); } - @Post('guest') + @Post("guest") @HttpCode(200) - @ApiOperation({ description: 'Login as a guest account' }) - @ApiOkResponse({ description: 'Successfully logged in', type: JwtToken }) + @ApiOperation({ description: "Login as a guest account" }) + @ApiOkResponse({ description: "Successfully logged in", type: JwtToken }) async guest(): Promise { const user = await this.usersService.createGuest(); await this.settingsService.createUserSetting(user.id); @@ -167,27 +169,27 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOperation({ description: 'Get the profile picture of connected user' }) - @ApiOkResponse({ description: 'The user profile picture' }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Get('me/picture') + @ApiOperation({ description: "Get the profile picture of connected user" }) + @ApiOkResponse({ description: "The user profile picture" }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Get("me/picture") async getProfilePicture(@Request() req: any, @Response() res: any) { return await this.usersService.getProfilePicture(req.user.id, res); } @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'The user profile picture' }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Post('me/picture') - @ApiOperation({ description: 'Upload a new profile picture' }) - @UseInterceptors(FileInterceptor('file')) + @ApiOkResponse({ description: "The user profile picture" }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Post("me/picture") + @ApiOperation({ description: "Upload a new profile picture" }) + @UseInterceptors(FileInterceptor("file")) async postProfilePicture( @Request() req: any, @UploadedFile( new ParseFilePipeBuilder() .addFileTypeValidator({ - fileType: 'jpeg', + fileType: "jpeg", }) .build({ errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, @@ -203,10 +205,10 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully logged in', type: User }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Get('me') - @ApiOperation({ description: 'Get the user info of connected user' }) + @ApiOkResponse({ description: "Successfully logged in", type: User }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Get("me") + @ApiOperation({ description: "Get the user info of connected user" }) async getProfile(@Request() req: any): Promise { const user = await this.usersService.user({ id: req.user.id }); if (!user) throw new InternalServerErrorException(); @@ -215,10 +217,10 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully edited profile', type: User }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Put('me') - @ApiOperation({ description: 'Edit the profile of connected user' }) + @ApiOkResponse({ description: "Successfully edited profile", type: User }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Put("me") + @ApiOperation({ description: "Edit the profile of connected user" }) editProfile( @Request() req: any, @Body() profile: Partial, @@ -241,20 +243,20 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully deleted', type: User }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Delete('me') - @ApiOperation({ description: 'Delete the profile of connected user' }) + @ApiOkResponse({ description: "Successfully deleted", type: User }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Delete("me") + @ApiOperation({ description: "Delete the profile of connected user" }) deleteSelf(@Request() req: any): Promise { return this.usersService.deleteUser({ id: req.user.id }); } @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully edited settings', type: Setting }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Patch('me/settings') - @ApiOperation({ description: 'Edit the settings of connected user' }) + @ApiOkResponse({ description: "Successfully edited settings", type: Setting }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Patch("me/settings") + @ApiOperation({ description: "Edit the settings of connected user" }) udpateSettings( @Request() req: any, @Body() settingUserDto: UpdateSettingDto, @@ -267,10 +269,10 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully edited settings', type: Setting }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Get('me/settings') - @ApiOperation({ description: 'Get the settings of connected user' }) + @ApiOkResponse({ description: "Successfully edited settings", type: Setting }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Get("me/settings") + @ApiOperation({ description: "Get the settings of connected user" }) async getSettings(@Request() req: any): Promise { const result = await this.settingsService.getUserSetting({ userId: +req.user.id, @@ -281,28 +283,31 @@ export class AuthController { @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully added liked song' }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Post('me/likes/:id') - addLikedSong(@Request() req: any, @Param('id') songId: number) { + @ApiOkResponse({ description: "Successfully added liked song" }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Post("me/likes/:id") + addLikedSong(@Request() req: any, @Param("id") songId: number) { return this.usersService.addLikedSong(+req.user.id, +songId); } @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully removed liked song' }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Delete('me/likes/:id') - removeLikedSong(@Request() req: any, @Param('id') songId: number) { + @ApiOkResponse({ description: "Successfully removed liked song" }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Delete("me/likes/:id") + removeLikedSong(@Request() req: any, @Param("id") songId: number) { return this.usersService.removeLikedSong(+req.user.id, +songId); } @UseGuards(JwtAuthGuard) @ApiBearerAuth() - @ApiOkResponse({ description: 'Successfully retrieved liked song' }) - @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Get('me/likes') - getLikedSongs(@Request() req: any) { - return this.usersService.getLikedSongs(+req.user.id); + @ApiOkResponse({ description: "Successfully retrieved liked song" }) + @ApiUnauthorizedResponse({ description: "Invalid token" }) + @Get("me/likes") + getLikedSongs(@Request() req: any, @Query("include") include: string) { + return this.usersService.getLikedSongs( + +req.user.id, + mapInclude(include, req, SongController.includableFields), + ); } } diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts index f0267ab..10c10e3 100644 --- a/back/src/users/users.service.ts +++ b/back/src/users/users.service.ts @@ -2,17 +2,17 @@ import { Injectable, InternalServerErrorException, NotFoundException, -} from '@nestjs/common'; -import { User, Prisma } from '@prisma/client'; -import { PrismaService } from 'src/prisma/prisma.service'; -import * as bcrypt from 'bcryptjs'; -import { createHash, randomUUID } from 'crypto'; -import { createReadStream, existsSync } from 'fs'; -import fetch from 'node-fetch'; +} from "@nestjs/common"; +import { User, Prisma } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import * as bcrypt from "bcryptjs"; +import { createHash, randomUUID } from "crypto"; +import { createReadStream, existsSync } from "fs"; +import fetch from "node-fetch"; @Injectable() export class UsersService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService) { } async user( userWhereUniqueInput: Prisma.UserWhereUniqueInput, @@ -53,7 +53,7 @@ export class UsersService { isGuest: true, // Not realyl clean but better than a separate table or breaking the api by adding nulls. email: null, - password: '', + password: "", }, }); } @@ -63,7 +63,7 @@ export class UsersService { data: Prisma.UserUpdateInput; }): Promise { const { where, data } = params; - if (typeof data.password === 'string') + if (typeof data.password === "string") data.password = await bcrypt.hash(data.password, 8); else if (data.password && data.password.set) data.password = await bcrypt.hash(data.password.set, 8); @@ -89,9 +89,9 @@ export class UsersService { const user = await this.user({ id: userId }); if (!user) throw new InternalServerErrorException(); if (!user.email) throw new NotFoundException(); - const hash = createHash('md5') + const hash = createHash("md5") .update(user.email.trim().toLowerCase()) - .digest('hex'); + .digest("hex"); const resp = await fetch( `https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`, ); @@ -105,9 +105,10 @@ export class UsersService { }); } - async getLikedSongs(userId: number) { + async getLikedSongs(userId: number, include?: Prisma.SongInclude) { return this.prisma.likedSongs.findMany({ where: { userId: userId }, + include: { song: include ? { include } : true }, }); } diff --git a/front/API.ts b/front/API.ts index ca6ec37..ef14683 100644 --- a/front/API.ts +++ b/front/API.ts @@ -4,7 +4,7 @@ import Chapter from './models/Chapter'; import Lesson from './models/Lesson'; import Genre, { GenreHandler } from './models/Genre'; import LessonHistory from './models/LessonHistory'; -import likedSong, { LikedSongHandler } from './models/LikedSong'; +import likedSong, { LikedSong, LikedSongHandler } from './models/LikedSong'; import Song, { SongHandler, SongInclude } from './models/Song'; import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory'; import User, { UserHandler } from './models/User'; @@ -297,13 +297,14 @@ export default class API { * Retrieve a song * @param songId the id to find the song */ - public static getSong(songId: number): Query { + public static getSong(songId: number, include?: SongInclude[]): Query { + include ??= []; return { - key: ['song', songId], + key: ['song', songId, include], exec: async () => API.fetch( { - route: `/song/${songId}`, + route: `/song/${songId}?include=${include!.join(',')}`, }, { handler: SongHandler } ), @@ -702,13 +703,14 @@ export default class API { }); } - public static getLikedSongs(): Query { + public static getLikedSongs(include?: SongInclude[]): Query { + include ??= []; return { - key: ['liked songs'], + key: ['liked songs', include], exec: () => API.fetch( { - route: '/auth/me/likes', + route: `/auth/me/likes?include=${include!.join(',')}`, }, { handler: ListHandler(LikedSongHandler) } ), diff --git a/front/Queries.ts b/front/Queries.ts index d72b777..348a678 100644 --- a/front/Queries.ts +++ b/front/Queries.ts @@ -70,11 +70,4 @@ const transformQuery = ( }; }; -const useQueries = ( - queries: readonly QueryOrQueryFn[], - options?: QueryOptions -) => { - return RQ.useQueries(queries.map((q) => buildRQuery(q, options))); -}; - -export { useQuery, useQueries, QueryRules, transformQuery }; +export { useQuery, QueryRules, transformQuery }; diff --git a/front/components/FavSongRow.tsx b/front/components/FavSongRow.tsx index db1b060..7c5a439 100644 --- a/front/components/FavSongRow.tsx +++ b/front/components/FavSongRow.tsx @@ -1,17 +1,18 @@ import { HStack, IconButton, Image, Text } from 'native-base'; import RowCustom from './RowCustom'; import TextButton from './TextButton'; -import { LikedSongWithDetails } from '../models/LikedSong'; import { MaterialIcons } from '@expo/vector-icons'; import API from '../API'; import DurationComponent from './DurationComponent'; +import Song from '../models/Song'; type FavSongRowProps = { - FavSong: LikedSongWithDetails; // TODO: remove Song + song: Song; + addedDate: Date; onPress: () => void; }; -const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => { +const FavSongRow = ({ song, addedDate, onPress }: FavSongRowProps) => { return ( @@ -20,8 +21,8 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => { flexGrow={0} pl={10} style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }} - source={{ uri: FavSong.details.cover }} - alt={FavSong.details.name} + source={{ uri: song.cover }} + alt={song.name} borderColor={'white'} borderWidth={1} /> @@ -45,7 +46,7 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => { bold fontSize="md" > - {FavSong.details.name} + {song.name} { }} fontSize={'sm'} > - {FavSong.addedDate.toLocaleDateString()} + {addedDate.toLocaleDateString()} - + { - API.removeLikedSong(FavSong.songId); + API.removeLikedSong(song.id); }} _icon={{ as: MaterialIcons, diff --git a/front/components/SearchResult.tsx b/front/components/SearchResult.tsx index 416756d..71e1607 100644 --- a/front/components/SearchResult.tsx +++ b/front/components/SearchResult.tsx @@ -12,7 +12,7 @@ import { } from 'native-base'; import { SafeAreaView } from 'react-native'; import { SearchContext } from '../views/SearchView'; -import { useQueries, useQuery } from '../Queries'; +import { useQuery } from '../Queries'; import { translate } from '../i18n/i18n'; import API from '../API'; import LoadingComponent, { LoadingView } from './Loading'; @@ -26,7 +26,6 @@ import { useNavigation } from '../Navigation'; import Artist from '../models/Artist'; import SongRow from '../components/SongRow'; import FavSongRow from './FavSongRow'; -import { LikedSongWithDetails } from '../models/LikedSong'; const swaToSongCardProps = (song: Song) => ({ songId: song.id, @@ -41,35 +40,7 @@ const HomeSearchComponent = () => { API.getSearchHistory(0, 12), { enabled: true } ); - const songSuggestions = useQuery(API.getSongSuggestions); - const songArtistSuggestions = useQueries( - songSuggestions.data - ?.filter((song) => song.artistId !== null) - .map(({ artistId }) => API.getArtist(artistId)) ?? [] - ); - const isLoadingSuggestions = useMemo( - () => songSuggestions.isLoading || songArtistSuggestions.some((q) => q.isLoading), - [songSuggestions, songArtistSuggestions] - ); - const suggestionsData = useMemo(() => { - if (isLoadingSuggestions) { - return []; - } - return ( - songSuggestions.data - ?.map((song): [Song, Artist | undefined] => [ - song, - songArtistSuggestions - .map((q) => q.data) - .filter((d) => d !== undefined) - .find((data) => data?.id === song.artistId), - ]) - // We do not need the song - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .filter(([song, artist]) => artist !== undefined) - .map(([song, artist]) => ({ ...song, artist: artist! })) ?? [] - ); - }, [songSuggestions, songArtistSuggestions]); + const songSuggestions = useQuery(API.getSongSuggestions(['artist'])); return ( @@ -94,11 +65,11 @@ const HomeSearchComponent = () => { {translate('songsToGetBetter')} - {isLoadingSuggestions ? ( + {!songSuggestions.data ? ( ) : ( )} @@ -219,19 +190,6 @@ const GenreSearchComponent = (props: ItemSearchComponentProps) => { const FavoritesComponent = () => { const navigation = useNavigation(); const favoritesQuery = useQuery(API.getLikedSongs()); - const songQueries = useQueries( - favoritesQuery.data - ?.map((favorite) => favorite.songId) - .map((songId) => API.getSong(songId)) ?? [] - ); - - const favSongWithDetails = favoritesQuery?.data - ?.map((favorite) => ({ - ...favorite, - details: songQueries.find((query) => query.data?.id == favorite.songId)?.data, - })) - .filter((favorite) => favorite.details !== undefined) - .map((likedSong) => likedSong as LikedSongWithDetails); if (favoritesQuery.isError) { navigation.navigate('Error'); @@ -247,13 +205,14 @@ const FavoritesComponent = () => { {translate('songsFilter')} - {favSongWithDetails?.map((songData) => ( + {favoritesQuery.data?.map((songData) => ( { - API.createSearchHistoryEntry(songData.details!.name, 'song'); //todo - navigation.navigate('Play', { songId: songData.details!.id }); + API.createSearchHistoryEntry(songData.song.name, 'song'); //todo + navigation.navigate('Play', { songId: songData.song!.id }); }} /> ))} diff --git a/front/models/LikedSong.ts b/front/models/LikedSong.ts index 386928a..d97cd18 100644 --- a/front/models/LikedSong.ts +++ b/front/models/LikedSong.ts @@ -1,33 +1,20 @@ import * as yup from 'yup'; import ResponseHandler from './ResponseHandler'; -import Song from './Song'; +import Song, { SongValidator } from './Song'; import Model, { ModelValidator } from './Model'; export const LikedSongValidator = yup .object({ songId: yup.number().required(), + song: yup.lazy(() => SongValidator.default(undefined)), addedDate: yup.date().required(), }) .concat(ModelValidator); -export const LikedSongHandler: ResponseHandler< - yup.InferType, - LikedSong -> = { - validator: LikedSongValidator, - transformer: (likedSong) => ({ - id: likedSong.id, - songId: likedSong.songId, - addedDate: likedSong.addedDate, - }), -}; -interface LikedSong extends Model { - songId: number; - addedDate: Date; -} +export type LikedSong = yup.InferType; -export interface LikedSongWithDetails extends LikedSong { - details: Song; -} +export const LikedSongHandler: ResponseHandler = { + validator: LikedSongValidator, +}; export default LikedSong; diff --git a/front/views/MusicView.tsx b/front/views/MusicView.tsx index 9b8f263..7c89a6b 100644 --- a/front/views/MusicView.tsx +++ b/front/views/MusicView.tsx @@ -22,26 +22,25 @@ import { LoadingView } from '../components/Loading'; export const FavoritesMusic = () => { const navigation = useNavigation(); - const playHistoryQuery = useQuery(API.getUserPlayHistory(['artist'])); + const likedSongs = useQuery(API.getLikedSongs(['artist'])); const musics = - playHistoryQuery.data - ?.map((x) => x.song) - .map((song: Song) => ({ - artist: song.artist!.name, - song: song.name, - image: song.cover, + likedSongs.data + ?.map((x) => ({ + artist: x.song.artist!.name, + song: x.song.name, + image: x.song.cover, level: 42, lastScore: 42, bestScore: 42, - liked: false, + liked: true, onLike: () => { console.log('onLike'); }, - onPlay: () => navigation.navigate('Play', { songId: song.id }), + onPlay: () => navigation.navigate('Play', { songId: x.song.id }), })) ?? []; - if (playHistoryQuery.isLoading) { + if (likedSongs.isLoading) { return ; } return ( diff --git a/front/views/ScoreView.tsx b/front/views/ScoreView.tsx index cd27420..dce956a 100644 --- a/front/views/ScoreView.tsx +++ b/front/views/ScoreView.tsx @@ -6,7 +6,7 @@ import TextButton from '../components/TextButton'; import API from '../API'; import CardGridCustom from '../components/CardGridCustom'; import SongCard from '../components/SongCard'; -import { useQueries, useQuery } from '../Queries'; +import { useQuery } from '../Queries'; import { LoadingView } from '../components/Loading'; import ScoreGraph from '../components/ScoreGraph'; @@ -29,23 +29,10 @@ type ScoreViewProps = { const ScoreView = (props: RouteProps) => { const { songId, overallScore, precision, score } = props; const navigation = useNavigation(); - const songQuery = useQuery(API.getSong(songId)); - const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), { - enabled: songQuery.data !== undefined, - }); - const recommendations = useQuery(API.getSongSuggestions); - const artistRecommendations = useQueries( - recommendations.data - ?.filter(({ artistId }) => artistId !== null) - .map((song) => API.getArtist(song.artistId)) ?? [] - ); + const songQuery = useQuery(API.getSong(songId, ['artist'])); + const recommendations = useQuery(API.getSongSuggestions(['artist'])); - if ( - !recommendations.data || - artistRecommendations.find(({ data }) => !data) || - !songQuery.data || - (songQuery.data.artistId && !artistQuery.data) - ) { + if (!recommendations.data || !songQuery.data) { return ; } if (songQuery.isError) { @@ -59,7 +46,7 @@ const ScoreView = (props: RouteProps) => { {songQuery.data.name} - {artistQuery.data?.name} + {songQuery.data.artist!.name} ) => { content={recommendations.data.map((i) => ({ cover: i.cover, name: i.name, - artistName: - artistRecommendations.find(({ data }) => data?.id == i.artistId)?.data - ?.name ?? '', + artistName: i.artist!.name, songId: i.id, }))} cardComponent={SongCard}