fixed mirgation + back-end + front end filter, heart shaped button and special FavSongRow
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -665,13 +665,19 @@ export default class API {
|
||||
}
|
||||
|
||||
public static async addLikedSong(songId: number): Promise<void> {
|
||||
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<void> {
|
||||
await API.fetch(
|
||||
{
|
||||
route: `/auth/me/likes/${songId}`,
|
||||
method: 'DELETE',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
<Image
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
pl={10}
|
||||
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
|
||||
source={{ uri: FavSong.details.cover }}
|
||||
alt={FavSong.details.name}
|
||||
borderColor={'white'}
|
||||
borderWidth={1}
|
||||
/>
|
||||
<HStack
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
space={6}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
}}
|
||||
isTruncated
|
||||
pl={5}
|
||||
maxW={'100%'}
|
||||
bold
|
||||
fontSize="md"
|
||||
>
|
||||
{FavSong.details.name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{FavSong.addedDate.toLocaleDateString()}
|
||||
</Text>
|
||||
</HStack>
|
||||
<IconButton colorScheme="primary" variant={'ghost'} borderRadius={'full'} onPress={() => {API.removeLikedSong(FavSong.songId)}}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
name: "favorite"
|
||||
}} />
|
||||
<TextButton
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
translate={{ translationKey: 'playBtn' }}
|
||||
colorScheme="primary"
|
||||
variant={'outline'}
|
||||
size="sm"
|
||||
mr={5}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</HStack>
|
||||
</RowCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavSongRow;
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 (
|
||||
<ScrollView>
|
||||
@@ -124,6 +132,7 @@ const SongsSearchComponent = (props: SongsSearchComponentProps) => {
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
isLiked={!favoritesQuery.data?.find((query) => 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')}
|
||||
</Text>
|
||||
<Box>
|
||||
{songQueries.map((songData) => (
|
||||
<SongRow
|
||||
song={songData} //todo
|
||||
{favSongWithDetails?.map((songData) => (
|
||||
<FavSongRow
|
||||
FavSong={songData}
|
||||
onPress={() => {
|
||||
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
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
@@ -53,6 +62,11 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
|
||||
{song.artistId ?? 'artist'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<IconButton colorScheme="rose" variant={'ghost'} borderRadius={'full'} onPress={() => { handleFavoriteButton(isLiked, song.id)}}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
name: isLiked ? "favorite-outline" : "favorite"
|
||||
}} />
|
||||
<TextButton
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
|
||||
+21
-33
@@ -1,41 +1,29 @@
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
import Song from './Song';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
|
||||
export const LikedSongItemValidator = yup.object({
|
||||
songId: yup.number().required(),
|
||||
addedDate: yup.date().required(),
|
||||
});
|
||||
export const LikedSongValidator = yup
|
||||
.object({
|
||||
songId: yup.number().required(),
|
||||
addedDate: yup.date().required(),
|
||||
}).concat(ModelValidator);
|
||||
|
||||
export const LikedSongItemHandler: ResponseHandler<
|
||||
yup.InferType<typeof LikedSongItemValidator>,
|
||||
LikedSongItem
|
||||
> = {
|
||||
validator: LikedSongItemValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
}),
|
||||
export const LikedSongHandler: ResponseHandler<yup.InferType<typeof LikedSongValidator>, 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<typeof LikedSongValidator>;
|
||||
|
||||
export const LikedSongHandler: ResponseHandler<LikedSong> = {
|
||||
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;
|
||||
@@ -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[];
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user