fixed mirgation + back-end + front end filter, heart shaped button and special FavSongRow

This commit is contained in:
danis
2023-09-17 20:57:10 +02:00
parent c21f5f0659
commit 431427d7ad
13 changed files with 235 additions and 78 deletions
@@ -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;
+11 -1
View File
@@ -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 {
+29 -4
View File
@@ -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)
}
}
+26 -11
View File
@@ -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
View File
@@ -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',
}
)
}
+78
View File
@@ -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;
+6 -1
View File
@@ -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'),
+21 -7
View File
@@ -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
}}
/>
))}
+16 -2
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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[];
-5
View File
@@ -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"