From 431427d7ad336fed3c5cdf0b1d5a9bdeb1ac98eb Mon Sep 17 00:00:00 2001 From: danis Date: Sun, 17 Sep 2023 20:57:10 +0200 Subject: [PATCH] fixed mirgation + back-end + front end filter, heart shaped button and special FavSongRow --- .../20230822074959_liked_songs/migration.sql | 8 -- .../migration.sql | 15 ++++ back/prisma/schema.prisma | 12 ++- back/src/auth/auth.controller.ts | 33 +++++++- back/src/users/users.service.ts | 37 ++++++--- front/API.ts | 16 ++-- front/components/FavSongRow.tsx | 78 +++++++++++++++++++ front/components/SearchBar.tsx | 7 +- front/components/SearchResult.tsx | 28 +++++-- front/components/SongRow.tsx | 18 ++++- front/models/LikedSong.ts | 54 +++++-------- front/views/SearchView.tsx | 2 +- front/yarn.lock | 5 -- 13 files changed, 235 insertions(+), 78 deletions(-) delete mode 100644 back/prisma/migrations/20230822074959_liked_songs/migration.sql create mode 100644 back/prisma/migrations/20230917180806_add_liked_songs/migration.sql create mode 100644 front/components/FavSongRow.tsx diff --git a/back/prisma/migrations/20230822074959_liked_songs/migration.sql b/back/prisma/migrations/20230822074959_liked_songs/migration.sql deleted file mode 100644 index 2439879..0000000 --- a/back/prisma/migrations/20230822074959_liked_songs/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `likedSongs` to the `User` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "User" ADD COLUMN "likedSongs" JSONB NOT NULL DEFAULT '{}'::jsonb; diff --git a/back/prisma/migrations/20230917180806_add_liked_songs/migration.sql b/back/prisma/migrations/20230917180806_add_liked_songs/migration.sql new file mode 100644 index 0000000..cb64ab1 --- /dev/null +++ b/back/prisma/migrations/20230917180806_add_liked_songs/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "LikedSongs" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "songId" INTEGER NOT NULL, + "addedDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LikedSongs_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LikedSongs" ADD CONSTRAINT "LikedSongs_songId_fkey" FOREIGN KEY ("songId") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/back/prisma/schema.prisma b/back/prisma/schema.prisma index e0a5ef4..c95dd0b 100644 --- a/back/prisma/schema.prisma +++ b/back/prisma/schema.prisma @@ -21,7 +21,16 @@ model User { SongHistory SongHistory[] searchHistory SearchHistory[] settings UserSettings? - likedSongs Json? + likedSongs LikedSongs[] +} + +model LikedSongs { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + song Song @relation(fields: [songId], references: [id], onDelete: Cascade) + songId Int + addedDate DateTime @default(now()) } model UserSettings { @@ -61,6 +70,7 @@ model Song { genre Genre? @relation(fields: [genreId], references: [id]) difficulties Json SongHistory SongHistory[] + likedByUsers LikedSongs[] } model SongHistory { diff --git a/back/src/auth/auth.controller.ts b/back/src/auth/auth.controller.ts index e2ba134..8f10851 100644 --- a/back/src/auth/auth.controller.ts +++ b/back/src/auth/auth.controller.ts @@ -205,15 +205,40 @@ export class AuthController { @ApiBearerAuth() @ApiOkResponse({ description: 'Successfully added liked song'}) @ApiUnauthorizedResponse({ description: 'Invalid token' }) - @Post('me/likes:id') + @Post('me/likes/:id') addLikedSong( @Request() req: any, - @Body() data: any, + @Param('id') songId: number ) { return this.usersService.addLikedSong( - req.user.id, - data.songId, + +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, + ) { + 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) + } } diff --git a/back/src/users/users.service.ts b/back/src/users/users.service.ts index f4db905..2513bff 100644 --- a/back/src/users/users.service.ts +++ b/back/src/users/users.service.ts @@ -101,19 +101,34 @@ export class UsersService { } async addLikedSong( - where: Prisma.UserWhereUniqueInput, + userId: number, songId: number, ) { - return this.prisma.user.update({ - where, - data: { - likedSongs: { - merde: { - songId: songId, - time: Date() - } - } + return this.prisma.likedSongs.create( + { + data: { songId: songId, userId: userId } } - }) + ) + } + + async getLikedSongs( + userId: number, + ) { + return this.prisma.likedSongs.findMany( + { + where: { userId: userId }, + } + ) + } + + async removeLikedSong( + userId: number, + songId: number, + ) { + return this.prisma.likedSongs.deleteMany( + { + where: { userId: userId, songId: songId }, + } + ) } } diff --git a/front/API.ts b/front/API.ts index 33b3fcf..3b311cc 100644 --- a/front/API.ts +++ b/front/API.ts @@ -665,13 +665,19 @@ export default class API { } public static async addLikedSong(songId: number): Promise { - const data = await API.fetch( + await API.fetch( { - route: `/auth/me/likes${songId}`, + route: `/auth/me/likes/${songId}`, method: 'POST', - body: { - songId: songId - } + } + ) + } + + public static async removeLikedSong(songId: number): Promise { + await API.fetch( + { + route: `/auth/me/likes/${songId}`, + method: 'DELETE', } ) } diff --git a/front/components/FavSongRow.tsx b/front/components/FavSongRow.tsx new file mode 100644 index 0000000..9010a88 --- /dev/null +++ b/front/components/FavSongRow.tsx @@ -0,0 +1,78 @@ +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'; + +type FavSongRowProps = { + FavSong: LikedSongWithDetails; // TODO: remove Song + onPress: () => void; +}; + +const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => { + return ( + + + {FavSong.details.name} + + + {FavSong.details.name} + + + {FavSong.addedDate.toLocaleDateString()} + + + {API.removeLikedSong(FavSong.songId)}} + _icon={{ + as: MaterialIcons, + name: "favorite" + }} /> + + + + ); +}; + +export default FavSongRow; \ No newline at end of file diff --git a/front/components/SearchBar.tsx b/front/components/SearchBar.tsx index b04cc97..7613ddb 100644 --- a/front/components/SearchBar.tsx +++ b/front/components/SearchBar.tsx @@ -5,7 +5,7 @@ import { translate } from '../i18n/i18n'; import { SearchContext } from '../views/SearchView'; import { debounce } from 'lodash'; -export type Filter = 'artist' | 'song' | 'genre' | 'all'; +export type Filter = 'artist' | 'song' | 'genre' | 'all' | 'favorites'; type FilterButton = { name: string; @@ -42,6 +42,11 @@ const SearchBar = () => { callback: () => updateFilter('all'), id: 'all', }, + { + name: translate('favoriteFilter'), + callback: () => updateFilter('favorites'), + id: 'favorites', + }, { name: translate('artistFilter'), callback: () => updateFilter('artist'), diff --git a/front/components/SearchResult.tsx b/front/components/SearchResult.tsx index 04987f3..cc45ebb 100644 --- a/front/components/SearchResult.tsx +++ b/front/components/SearchResult.tsx @@ -25,6 +25,9 @@ import Song, { SongWithArtist } from '../models/Song'; import { useNavigation } from '../Navigation'; import Artist from '../models/Artist'; import SongRow from '../components/SongRow'; +import RowCustom from './RowCustom'; +import FavSongRow from './FavSongRow'; +import { LikedSongWithDetails } from '../models/LikedSong'; const swaToSongCardProps = (song: SongWithArtist) => ({ songId: song.id, @@ -110,8 +113,13 @@ type SongsSearchComponentProps = { }; const SongsSearchComponent = (props: SongsSearchComponentProps) => { - const { songData } = React.useContext(SearchContext); const navigation = useNavigation(); + const { songData } = React.useContext(SearchContext); + const favoritesQuery = useQuery(API.getLikedSongs()); + // const songQueryWithFavorite = songData?.map((songs) => ({ + // ...songs, + // isLiked: !favoritesQuery.data?.find((query) => query?.songId == songs.id) + // })) return ( @@ -124,6 +132,7 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => { query?.songId == comp.id)} onPress={() => { API.createSearchHistoryEntry(comp.name, 'song'); navigation.navigate('Song', { songId: comp.id }); @@ -207,13 +216,18 @@ type FavoriteComponentProps = { }; const FavoritesComponent = (props: FavoriteComponentProps) => { + const navigation = useNavigation(); const favoritesQuery = useQuery(API.getLikedSongs()); const songQueries = useQueries( favoritesQuery.data?.map((favorite) => favorite.songId).map((songId) => API.getSong(songId)) ?? [] ); - const navigation = useNavigation(); + 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'); @@ -229,12 +243,12 @@ const FavoritesComponent = (props: FavoriteComponentProps) => { {translate('songsFilter')} - {songQueries.map((songData) => ( - ( + { - API.createSearchHistoryEntry(comp.name, 'song'); //todo - navigation.navigate('Song', { songId: comp.id }); //todo + API.createSearchHistoryEntry(songData.details!.name, 'song'); //todo + navigation.navigate('Song', { songId: songData.details!.id }); //todo }} /> ))} diff --git a/front/components/SongRow.tsx b/front/components/SongRow.tsx index 4a61c83..b661a63 100644 --- a/front/components/SongRow.tsx +++ b/front/components/SongRow.tsx @@ -1,14 +1,23 @@ -import { HStack, Image, Text } from 'native-base'; +import { HStack, IconButton, Image, Text } from 'native-base'; import Song, { SongWithArtist } from '../models/Song'; import RowCustom from './RowCustom'; import TextButton from './TextButton'; +import { MaterialIcons } from '@expo/vector-icons'; +import API from '../API'; type SongRowProps = { song: Song | SongWithArtist; // TODO: remove Song + isLiked: boolean; onPress: () => void; + handleLike: () => void; }; -const SongRow = ({ song, onPress }: SongRowProps) => { +const handleFavoriteButton = (state: boolean, songId: number): void => { + if (state == false) API.removeLikedSong(songId); + else API.addLikedSong(songId); +} + +const SongRow = ({ song, onPress, isLiked }: SongRowProps) => { return ( @@ -53,6 +62,11 @@ const SongRow = ({ song, onPress }: SongRowProps) => { {song.artistId ?? 'artist'} + { handleFavoriteButton(isLiked, song.id)}} + _icon={{ + as: MaterialIcons, + name: isLiked ? "favorite-outline" : "favorite" + }} /> , - LikedSongItem - > = { - validator: LikedSongItemValidator, - transformer: (value) => ({ - ...value, - }), +export const LikedSongHandler: ResponseHandler, LikedSong> = { + validator: LikedSongValidator, + transformer: (likedSong) => ({ + id: likedSong.id, + songId: likedSong.songId, + addedDate: likedSong.addedDate, + }), +}; +interface LikedSong extends Model { + songId: number; + addedDate: Date; }; -export const LikedSongValidator = yup.object({ - songId: yup.number().required(), - history: yup.array(LikedSongItemValidator).required(), -}); - -export type LikedSong = yup.InferType; - -export const LikedSongHandler: ResponseHandler = { - validator: LikedSongValidator, - transformer: (value) => ({ - ...value, - history: value.history.map((item) => - LikedSongItemHandler.transformer(item) - ), - }), -}; - -export type LikedSongItem = { - songId: number; - addedDate: Date; -}; +export interface LikedSongWithDetails extends LikedSong { + details: Song; +} export default LikedSong; \ No newline at end of file diff --git a/front/views/SearchView.tsx b/front/views/SearchView.tsx index 21e7c3c..ddb7fc4 100644 --- a/front/views/SearchView.tsx +++ b/front/views/SearchView.tsx @@ -14,7 +14,7 @@ import LikedSong from '../models/LikedSong'; interface SearchContextType { filter: 'artist' | 'song' | 'genre' | 'all' | 'favorites'; - updateFilter: (newData: 'artist' | 'song' | 'genre' | 'all' | 'favorites' s) => void; + updateFilter: (newData: 'artist' | 'song' | 'genre' | 'all' | 'favorites') => void; stringQuery: string; updateStringQuery: (newData: string) => void; songData: Song[]; diff --git a/front/yarn.lock b/front/yarn.lock index 182e627..224b678 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -9138,11 +9138,6 @@ expo-keep-awake@~11.0.1: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3" integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A== -expo-linear-gradient@^12.3.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.3.0.tgz#7abd8fedbf0138c86805aebbdfbbf5e5fa865f19" - integrity sha512-f9e+Oxe5z7fNQarTBZXilMyswlkbYWQHONVfq8MqmiEnW3h9XsxxmVJLG8uVQSQPUsbW+x1UUT/tnU6mkMWeLg== - expo-linking@~3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"