diff --git a/src/Api/src/controllers/userController.ts b/src/Api/src/controllers/userController.ts index 55614d4..4eea2d7 100644 --- a/src/Api/src/controllers/userController.ts +++ b/src/Api/src/controllers/userController.ts @@ -42,6 +42,8 @@ class UserController implements IController { this.router.get(`${this.path}/musics`, authenticator, this.getMusics); this.router.put(`${this.path}/name`, authenticator, this.setName); this.router.put(`${this.path}/email`, authenticator, this.setEmail); + this.router.put(`${this.path}/image`, authenticator, this.setImage); + this.router.put(`${this.path}/password`, authenticator, this.setPassword); } @@ -273,6 +275,40 @@ class UserController implements IController { next(new HttpException(409, error.message)); } } + + private setImage = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const { image } = req.body; + + await this.userService.setImage(_id, image); + + res.status(200).json({ message: 'Image updated successfully' }); + } catch (error: any) { + next(new HttpException(500, error.message)); + } + } + + private setPassword = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const { oldPassword, newPassword } = req.body; + + await this.userService.setPassword(_id, oldPassword, newPassword); + + res.status(200).json({ message: 'Password updated successfully' }); + } catch (error: any) { + next(new HttpException(500, error.message)); + } + } } export default UserController; diff --git a/src/Api/src/services/UserService.ts b/src/Api/src/services/UserService.ts index 8554155..b03cfee 100644 --- a/src/Api/src/services/UserService.ts +++ b/src/Api/src/services/UserService.ts @@ -95,8 +95,7 @@ class UserService { try { await this.user.findByIdAndUpdate( userId, - { name: newName }, - { new: true } + { name: newName } ); } catch (error) { throw new Error(error.message); @@ -107,13 +106,39 @@ class UserService { try { await this.user.findByIdAndUpdate( userId, - { email: newEmail }, - { new: true } + { email: newEmail } ); } catch (error) { throw new Error(error.message); } } + + public async setImage(userId: string, newImage: string): Promise { + try { + await this.user.findByIdAndUpdate( + userId, + { image: newImage } + ); + } catch (error) { + throw new Error(error.message); + } + } + + public async setPassword(userId: string, oldPassword: string, newPassword: string): Promise { + try { + + const user = await this.user.findById(userId); + + if (await user.isValidPassword(oldPassword)) { + user.password = newPassword; + await user.save(); + } else { + throw new Error('Old password does not match.'); + } + } catch (error) { + throw new Error(error.message); + } + } } export default UserService; \ No newline at end of file diff --git a/src/FLAD/assets/images/explicit_icon.png b/src/FLAD/assets/images/explicit_icon.png new file mode 100644 index 0000000..f1cef16 Binary files /dev/null and b/src/FLAD/assets/images/explicit_icon.png differ diff --git a/src/FLAD/assets/images/flad_logo_animation.gif b/src/FLAD/assets/images/flad_logo_animation.gif deleted file mode 100644 index 8df51f0..0000000 Binary files a/src/FLAD/assets/images/flad_logo_animation.gif and /dev/null differ diff --git a/src/FLAD/assets/images/pause_icon.png b/src/FLAD/assets/images/pause_icon.png new file mode 100644 index 0000000..8adabf9 Binary files /dev/null and b/src/FLAD/assets/images/pause_icon.png differ diff --git a/src/FLAD/assets/images/play_icon.png b/src/FLAD/assets/images/play_icon.png new file mode 100644 index 0000000..54bfff9 Binary files /dev/null and b/src/FLAD/assets/images/play_icon.png differ diff --git a/src/FLAD/assets/images/save_icon.png b/src/FLAD/assets/images/save_icon.png new file mode 100644 index 0000000..7891b53 Binary files /dev/null and b/src/FLAD/assets/images/save_icon.png differ diff --git a/src/FLAD/assets/images/save_icon_full.png b/src/FLAD/assets/images/save_icon_full.png new file mode 100644 index 0000000..9287096 Binary files /dev/null and b/src/FLAD/assets/images/save_icon_full.png differ diff --git a/src/FLAD/assets/images/share_icon.png b/src/FLAD/assets/images/share_icon.png new file mode 100644 index 0000000..a100e16 Binary files /dev/null and b/src/FLAD/assets/images/share_icon.png differ diff --git a/src/FLAD/components/CardMusicComponent.tsx b/src/FLAD/components/CardMusicComponent.tsx index 9a7b7bc..525d370 100644 --- a/src/FLAD/components/CardMusicComponent.tsx +++ b/src/FLAD/components/CardMusicComponent.tsx @@ -4,12 +4,11 @@ import { useSelector } from 'react-redux'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; import normalize from './Normalize'; +import Music from '../model/Music'; +import Artist from '../model/Artist'; type CardMusicProps = { - image: string; - title: string; - description: string; - id: string; + music: Music } export default function CardMusic(props: CardMusicProps) { @@ -17,7 +16,6 @@ export default function CardMusic(props: CardMusicProps) { const isDark = useSelector(state => state.userReducer.dark); const style = isDark ? colorsDark : colorsLight; - const source = typeof props.image === 'string' ? { uri: props.image } : props.image; const styles = StyleSheet.create({ container: { flexDirection: 'row', @@ -62,11 +60,11 @@ export default function CardMusic(props: CardMusicProps) { - + - {props.title} - {props.description} + {props.music.name} + {props.music.artists.map((artist: Artist) => artist.name).join(', ')} ); diff --git a/src/FLAD/components/HorizontalFlatList.tsx b/src/FLAD/components/HorizontalFlatList.tsx deleted file mode 100644 index 5df0a3d..0000000 --- a/src/FLAD/components/HorizontalFlatList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { View, StyleSheet, Text, FlatList } from "react-native"; -import { RenderCellProps } from "./littleCard"; - - -interface HorizontalFlatListProps { - children: (props: RenderCellProps) => React.ReactElement - title: string; - data: any[]; -} -export const HorizontalFlatList = ({ title, data, children: RenderCell }: HorizontalFlatListProps) => { - - return ( - - {title} - item.id} - renderItem={({ item }) => RenderCell(item)} /> - ); -}; -const styles = StyleSheet.create({ - similarSection: { - paddingTop: 16 - }, - similarTitle: { - color: "#FFF", - paddingLeft: 8, - fontSize: 24, - fontWeight: "600", - paddingBottom: 16 - } - -}); \ No newline at end of file diff --git a/src/FLAD/components/SimilarMusicComponent.tsx b/src/FLAD/components/SimilarMusicComponent.tsx new file mode 100644 index 0000000..dbc5f1c --- /dev/null +++ b/src/FLAD/components/SimilarMusicComponent.tsx @@ -0,0 +1,36 @@ +import { View, Text, StyleSheet, Image } from 'react-native'; +import Music from '../model/Music'; +import normalize from './Normalize'; + +export interface RenderCellProps { + music : Music; +} +export const SimilarMusic = (props: RenderCellProps) => { + return ( + + + {props.music.name} + + + ) +} + +const styles = StyleSheet.create({ + + similarContainer: { + marginHorizontal: normalize(7) + }, + similarTitle: { + color: "#DADADA", + paddingTop: 5, + paddingLeft: 5, + fontWeight: "600", + maxWidth: normalize(130), + fontSize: normalize(14) + }, + similarPoster: { + height: normalize(130), + width: normalize(130), + borderRadius: 12 + } +}) diff --git a/src/FLAD/components/littleCard.tsx b/src/FLAD/components/littleCard.tsx deleted file mode 100644 index 42ec5e9..0000000 --- a/src/FLAD/components/littleCard.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { View, Text, StyleSheet, Image } from 'react-native'; - -export interface RenderCellProps { - data : any; -} -export const LittleCard = (props: RenderCellProps) => { - return ( - - - {props.data.name} - - - ) -} - -const styles = StyleSheet.create({ - - similarContainer: { - marginHorizontal: 7 - }, - similarTitleFilm: { - color: "#DADADA", - paddingTop: 5, - fontWeight: "300" - }, - similarPoster: { - height: 160, - width: 160, - borderRadius: 16 - } -}) diff --git a/src/FLAD/models/Artist.ts b/src/FLAD/model/Artist.ts similarity index 68% rename from src/FLAD/models/Artist.ts rename to src/FLAD/model/Artist.ts index a06698a..991c22d 100644 --- a/src/FLAD/models/Artist.ts +++ b/src/FLAD/model/Artist.ts @@ -1,13 +1,11 @@ export default class Artist { private _id: string; private _name: string; - private _image: string; private _url: string; - constructor(id: string, name: string, image: string, url: string) { + constructor(id: string, name: string, url: string) { this._id = id; this._name = name; - this._image = image; this._url = url; } @@ -27,14 +25,6 @@ export default class Artist { this._name = value; } - get image(): string { - return this._image; - } - - set image(value: string) { - this._image = value; - } - get url(): string { return this._url; } diff --git a/src/FLAD/models/Music.ts b/src/FLAD/model/Music.ts similarity index 88% rename from src/FLAD/models/Music.ts rename to src/FLAD/model/Music.ts index f926eeb..83df62a 100644 --- a/src/FLAD/models/Music.ts +++ b/src/FLAD/model/Music.ts @@ -6,6 +6,7 @@ export default class Music { private _url: string; private _artists: Artist[]; private _cover: string; + private _littleCover: string; private _date: number; private _duration: number; private _explicit: boolean = false; @@ -17,6 +18,7 @@ export default class Music { url: string, artists: Artist[], cover: string, + littleCover: string, date: number, duration: number, explicit: boolean, @@ -27,6 +29,7 @@ export default class Music { this._url = url; this._artists = artists; this._cover = cover; + this._littleCover = littleCover; this._date = date; this._duration = duration; this._explicit = explicit; @@ -73,6 +76,14 @@ export default class Music { this._cover = value; } + get littleCover(): string { + return this._littleCover; + } + + set littleCover(value: string) { + this._littleCover = value; + } + get date(): number { return this._date; } diff --git a/src/FLAD/models/MusicServiceProvider.ts b/src/FLAD/model/MusicServiceProvider.ts similarity index 100% rename from src/FLAD/models/MusicServiceProvider.ts rename to src/FLAD/model/MusicServiceProvider.ts diff --git a/src/FLAD/models/Person.ts b/src/FLAD/model/Person.ts similarity index 100% rename from src/FLAD/models/Person.ts rename to src/FLAD/model/Person.ts diff --git a/src/FLAD/models/Spot.ts b/src/FLAD/model/Spot.ts similarity index 100% rename from src/FLAD/models/Spot.ts rename to src/FLAD/model/Spot.ts diff --git a/src/FLAD/models/User.ts b/src/FLAD/model/User.ts similarity index 100% rename from src/FLAD/models/User.ts rename to src/FLAD/model/User.ts diff --git a/src/FLAD/models/mapper/ArtistMapper.ts b/src/FLAD/model/mapper/ArtistMapper.ts similarity index 50% rename from src/FLAD/models/mapper/ArtistMapper.ts rename to src/FLAD/model/mapper/ArtistMapper.ts index ea06e9f..d644463 100644 --- a/src/FLAD/models/mapper/ArtistMapper.ts +++ b/src/FLAD/model/mapper/ArtistMapper.ts @@ -2,6 +2,6 @@ import Artist from "../Artist"; export default class ArtistMapper { static toModel(artist: any): Artist { - return new Artist(artist.id, artist.name, (artist?.images?.[0]?.url ?? ""), artist.external_urls.spotify); + return new Artist(artist.id, artist.name, artist.external_urls.spotify); } } \ No newline at end of file diff --git a/src/FLAD/models/mapper/MusicMapper.ts b/src/FLAD/model/mapper/MusicMapper.ts similarity index 85% rename from src/FLAD/models/mapper/MusicMapper.ts rename to src/FLAD/model/mapper/MusicMapper.ts index 1a5ce0e..c8f0e01 100644 --- a/src/FLAD/models/mapper/MusicMapper.ts +++ b/src/FLAD/model/mapper/MusicMapper.ts @@ -4,12 +4,14 @@ import ArtistMapper from "./ArtistMapper"; export default class MusicMapper { static toModel(music: any): Music { const artists = music.artists.map((artist: any) => ArtistMapper.toModel(artist)); + const last = music.album.images.length - 1; return new Music( music.id, music.name, music.external_urls.spotify, artists, music.album.images[0].url, + music.album.images[last].url, music.album.release_date.split('-')[0], music.duration_ms / 1000, music.explicit, diff --git a/src/FLAD/models/mapper/SpotMapper.ts b/src/FLAD/model/mapper/SpotMapper.ts similarity index 100% rename from src/FLAD/models/mapper/SpotMapper.ts rename to src/FLAD/model/mapper/SpotMapper.ts diff --git a/src/FLAD/models/mapper/UserMapper.ts b/src/FLAD/model/mapper/UserMapper.ts similarity index 100% rename from src/FLAD/models/mapper/UserMapper.ts rename to src/FLAD/model/mapper/UserMapper.ts diff --git a/src/FLAD/package.json b/src/FLAD/package.json index 6869128..960a188 100644 --- a/src/FLAD/package.json +++ b/src/FLAD/package.json @@ -18,6 +18,7 @@ "expo": "~47.0.12", "expo-auth-session": "~3.8.0", "expo-av": "~13.0.3", + "expo-blur": "~12.0.1", "expo-cli": "^6.3.10", "expo-haptics": "~12.0.1", "expo-image-picker": "~14.0.2", diff --git a/src/FLAD/redux/actions/appActions.ts b/src/FLAD/redux/actions/appActions.ts index 2e530c5..a4e662a 100644 --- a/src/FLAD/redux/actions/appActions.ts +++ b/src/FLAD/redux/actions/appActions.ts @@ -1,5 +1,5 @@ -import Music from "../../models/Music"; -import { Spot } from "../../models/Spot"; +import Music from "../../model/Music"; +import { Spot } from "../../model/Spot"; import { favoritesTypes } from "../types/favoritesTypes"; import { spotifyTypes } from "../types/spotifyTypes"; @@ -15,11 +15,4 @@ export const setFavoriteMusic = (spots: Spot[]) => { type: favoritesTypes.GET_FAVORITE_MUSICS, payload: spots, }; -} - -export const addFavoritesMusic = (music: Music) => { - return { - type: favoritesTypes.ADD_FAVORITE_MUSICS, - payload: music, - }; } \ No newline at end of file diff --git a/src/FLAD/redux/actions/spotActions.tsx b/src/FLAD/redux/actions/spotActions.tsx index 213f1ec..677691a 100644 --- a/src/FLAD/redux/actions/spotActions.tsx +++ b/src/FLAD/redux/actions/spotActions.tsx @@ -1,5 +1,4 @@ -import Music from "../../models/Music"; -import { Spot } from "../../models/Spot"; +import { Spot } from "../../model/Spot"; import { spotTypes } from "../types/spotTypes"; export const setSpotList = (spotList: Spot[]) => { diff --git a/src/FLAD/redux/actions/userActions.tsx b/src/FLAD/redux/actions/userActions.tsx index e8ec712..f31cbed 100644 --- a/src/FLAD/redux/actions/userActions.tsx +++ b/src/FLAD/redux/actions/userActions.tsx @@ -1,4 +1,4 @@ -import { User } from "../../models/User"; +import { User } from "../../model/User"; import { userTypes } from "../types/userTypes"; export interface LoginCredentials { diff --git a/src/FLAD/redux/reducers/appReducer.tsx b/src/FLAD/redux/reducers/appReducer.tsx index 864e97b..8ea05f7 100644 --- a/src/FLAD/redux/reducers/appReducer.tsx +++ b/src/FLAD/redux/reducers/appReducer.tsx @@ -1,4 +1,4 @@ -import { Spot } from "../../models/Spot"; +import { Spot } from "../../model/Spot"; import { favoritesTypes } from "../types/favoritesTypes"; import { spotifyTypes } from "../types/spotifyTypes"; import { spotTypes } from "../types/spotTypes"; @@ -14,10 +14,8 @@ const appReducer = (state = initialState, action: any) => { switch (action.type) { case favoritesTypes.GET_FAVORITE_MUSICS: return { ...state, favoriteMusic: action.payload }; - case favoritesTypes.REMOVE_FAVORITE_MUSICS: - return { ...state, favoriteMusic: state.favoriteMusic }; case spotTypes.FETCH_SPOT: - const uniqueSpots = action.payload.filter((spot) => { + const uniqueSpots = action.payload.filter((spot: Spot) => { return !state.spot.some((s) => s.userSpotifyId === spot.userSpotifyId && s.music.id === spot.music.id); }); const updatedSpotList = [...uniqueSpots, ...state.spot]; diff --git a/src/FLAD/redux/reducers/userReducer.tsx b/src/FLAD/redux/reducers/userReducer.tsx index 874aa54..7be604d 100644 --- a/src/FLAD/redux/reducers/userReducer.tsx +++ b/src/FLAD/redux/reducers/userReducer.tsx @@ -1,5 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { User } from "../../models/User"; +import { User } from "../../model/User"; import { userTypes } from "../types/userTypes"; const initialState = { diff --git a/src/FLAD/redux/store.tsx b/src/FLAD/redux/store.tsx index 99b5547..ae9a8a7 100644 --- a/src/FLAD/redux/store.tsx +++ b/src/FLAD/redux/store.tsx @@ -1,10 +1,6 @@ import { configureStore } from '@reduxjs/toolkit' import appReducer from './reducers/appReducer'; import userReducer from './reducers/userReducer'; -import { spotTypes } from './types/spotTypes'; -import { userTypes } from './types/userTypes'; -import { spotifyTypes } from './types/spotifyTypes'; -import { favoritesTypes } from './types/favoritesTypes'; // Reference here all your application reducers const reducer = { @@ -15,13 +11,10 @@ const reducer = { const store = configureStore({ // @ts-ignore reducer: reducer, - middleware: (getDefaultMiddleware) => getDefaultMiddleware({ - serializableCheck: { - ignoredActions: [spotTypes.FETCH_SPOT, spotifyTypes.GET_USER_CURRENT_MUSIC, favoritesTypes.ADD_FAVORITE_MUSICS, favoritesTypes.REMOVE_FAVORITE_MUSICS, spotTypes.REMOVE_SPOT, userTypes.LOGIN], - ignoredActionPaths: ['appReducer'], - ignoredPaths: ['appReducer', 'userReducer'] - } - }) + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: false, + }) },); export default store; \ No newline at end of file diff --git a/src/FLAD/redux/thunk/appThunk.tsx b/src/FLAD/redux/thunk/appThunk.tsx index fb49b75..1630200 100644 --- a/src/FLAD/redux/thunk/appThunk.tsx +++ b/src/FLAD/redux/thunk/appThunk.tsx @@ -1,11 +1,11 @@ import axios from "axios"; import * as SecureStore from 'expo-secure-store'; -import { Spot } from "../../models/Spot"; +import { Spot } from "../../model/Spot"; import configs from "../../constants/config"; -import { MusicServiceProvider } from "../../models/MusicServiceProvider"; +import { MusicServiceProvider } from "../../model/MusicServiceProvider"; import { setFavoriteMusic, setUserCurrentMusic } from "../actions/appActions"; import { setAccessError, setErrorEmptyMusic } from "../actions/userActions"; -import { SpotMapper } from "../../models/mapper/SpotMapper"; +import { SpotMapper } from "../../model/mapper/SpotMapper"; export const getUserCurrentMusic = () => { //@ts-ignore @@ -26,12 +26,12 @@ export const getUserCurrentMusic = () => { const music = await MusicServiceProvider.musicService.getMusicById(idTrack); dispatch(setUserCurrentMusic(music)) } catch (error: any) { - console.error("Error retrieving music currently listened : " + error); switch (error.response.status) { case 403: dispatch(setAccessError(true)); break; default: + console.error("Error retrieving music currently listened : " + error); dispatch(setAccessError(true)); break; } diff --git a/src/FLAD/redux/thunk/authThunk.tsx b/src/FLAD/redux/thunk/authThunk.tsx index 9c29c03..46a9ac3 100644 --- a/src/FLAD/redux/thunk/authThunk.tsx +++ b/src/FLAD/redux/thunk/authThunk.tsx @@ -2,8 +2,8 @@ import axios from "axios"; import configs from "../../constants/config"; import { LoginCredentials, RegisterCredentials, restoreToken, userLogin, userLogout, setErrorLogin, setErrorSignup, setErrorNetwork } from "../actions/userActions"; import * as SecureStore from 'expo-secure-store'; -import { UserMapper } from "../../models/mapper/UserMapper"; -import { MusicServiceProvider } from "../../models/MusicServiceProvider"; +import { UserMapper } from "../../model/mapper/UserMapper"; +import { MusicServiceProvider } from "../../model/MusicServiceProvider"; const keyRemember = 'rememberUser'; @@ -34,7 +34,6 @@ export const register = (resgisterCredential: RegisterCredentials) => { MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify); dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { - console.error("Error : " + error.message); switch (error.response.status) { case 400: dispatch(setErrorSignup("Email non valide !")); @@ -46,6 +45,7 @@ export const register = (resgisterCredential: RegisterCredentials) => { dispatch(setErrorSignup("Compte Spotify non autorisé !")); break; default: + console.error("Error : " + error.message); dispatch(setErrorSignup("Erreur lors de l'inscription !")); break; } @@ -86,12 +86,12 @@ export const login = (loginCredential: LoginCredentials, remember: boolean) => { MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify); dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { - console.error("Error : " + error.message); switch (error.response.status) { case 400: dispatch(setErrorLogin(true)); break; default: + console.error("Error : " + error.message); dispatch(setErrorNetwork(true)); break; } diff --git a/src/FLAD/redux/thunk/userThunk.tsx b/src/FLAD/redux/thunk/userThunk.tsx index 369b225..0ea79f6 100644 --- a/src/FLAD/redux/thunk/userThunk.tsx +++ b/src/FLAD/redux/thunk/userThunk.tsx @@ -2,7 +2,7 @@ import axios from "axios"; import configs from "../../constants/config"; import { setDarkMode, setErrorNetwork, setErrorUpdateMessage, userLogin } from "../actions/userActions"; import * as SecureStore from 'expo-secure-store'; -import { UserMapper } from "../../models/mapper/UserMapper"; +import { UserMapper } from "../../model/mapper/UserMapper"; export const darkMode = (value: boolean) => { //@ts-ignore @@ -28,13 +28,12 @@ export const setName = (name: string) => { dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { - console.error("Error : " + error.message); switch (error.response.status) { case 409: dispatch(setErrorUpdateMessage("Nom déjà utilisé.")) break; default: - dispatch(setErrorNetwork(true)); + console.error("Error : " + error.message); break; } } @@ -58,13 +57,64 @@ export const setMail = (email: string) => { ) dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { - console.error("Error : " + error.message); switch (error.response.status) { case 409: dispatch(setErrorUpdateMessage("Email déjà utilisé.")) break; default: - dispatch(setErrorNetwork(true)); + console.error("Error : " + error.message); + break; + } + } + } +} + +export const setPassword = (oldPassword: string, newPassword: string) => { + //@ts-ignore + return async dispatch => { + try { + let token: string | null = await SecureStore.getItemAsync(configs.key); + const headers = { + 'Authorization': 'Bearer ' + token + }; + await axios.put(configs.API_URL + '/user/password', { oldPassword, newPassword }, { headers }); + + } catch (error: any) { + switch (error.response.status) { + case 500: + dispatch(setErrorUpdateMessage("Mot de passe incorrect.")) + break; + default: + console.error("Error : " + error.message); + break; + } + } + } +} + +export const setImage = (image: string) => { + //@ts-ignore + return async dispatch => { + try { + let token: string | null = await SecureStore.getItemAsync(configs.key); + const headers = { + 'Authorization': 'Bearer ' + token + }; + + await axios.put(configs.API_URL + '/user/image', { image }, { headers }); + + const user = await axios.get( + configs.API_URL + '/user', + { headers } + ) + dispatch(userLogin(UserMapper.toModel(user.data.data))); + } catch (error: any) { + switch (error.response.status) { + case 413: + dispatch(setErrorUpdateMessage("Taille de l'image trop grande :\n" + image.length / 1000 + " Ko. Max 100 Ko.")) + break; + default: + console.error("Error : " + error.message); break; } } diff --git a/src/FLAD/redux/types/favoritesTypes.tsx b/src/FLAD/redux/types/favoritesTypes.tsx index 7d747f5..4db8c67 100644 --- a/src/FLAD/redux/types/favoritesTypes.tsx +++ b/src/FLAD/redux/types/favoritesTypes.tsx @@ -1,5 +1,4 @@ export const favoritesTypes = { GET_FAVORITE_MUSICS: 'GET_FAVORITE_MUSICS', ADD_FAVORITE_MUSICS: 'ADD_FAVORITE_MUSICS', - REMOVE_FAVORITE_MUSICS: 'REMOVE_FAVORITE_MUSICS', } \ No newline at end of file diff --git a/src/FLAD/redux/types/playlistTypes.tsx b/src/FLAD/redux/types/playlistTypes.tsx deleted file mode 100644 index c22f6a6..0000000 --- a/src/FLAD/redux/types/playlistTypes.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const playlistTypes = { - SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST', -} \ No newline at end of file diff --git a/src/FLAD/screens/ConversationScreen.tsx b/src/FLAD/screens/ConversationScreen.tsx index d260dc9..634210d 100644 --- a/src/FLAD/screens/ConversationScreen.tsx +++ b/src/FLAD/screens/ConversationScreen.tsx @@ -5,6 +5,7 @@ import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; import Friend from "../components/FriendComponent"; import normalize from '../components/Normalize'; +import { useSafeAreaInsets } from "react-native-safe-area-context"; export default function ConversationScreen() { @@ -22,10 +23,13 @@ export default function ConversationScreen() { const style = isDark ? colorsDark : colorsLight; + const insets = useSafeAreaInsets(); + const styles = StyleSheet.create({ mainSafeArea: { flex: 1, backgroundColor: style.body, + paddingTop: insets.top }, titleContainer: { marginTop: 10, diff --git a/src/FLAD/screens/DetailScreen.tsx b/src/FLAD/screens/DetailScreen.tsx index 48e6b4f..7e438b9 100644 --- a/src/FLAD/screens/DetailScreen.tsx +++ b/src/FLAD/screens/DetailScreen.tsx @@ -1,75 +1,101 @@ -import { useNavigation } from "@react-navigation/native"; -import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Pressable, Share, Alert } from "react-native"; +import { useIsFocused, useNavigation } from "@react-navigation/native"; +import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Share, Alert, SafeAreaView, Linking, FlatList, ActivityIndicator } from "react-native"; import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle, withSpring } from "react-native-reanimated"; import { Audio } from 'expo-av'; import { useEffect, useState } from "react"; import normalize from '../components/Normalize'; -import Music from "../models/Music"; +import Music from "../model/Music"; import { LinearGradient } from "expo-linear-gradient"; -import { Feather as Icon } from "@expo/vector-icons"; -import { MusicServiceProvider } from "../models/MusicServiceProvider"; -import { HorizontalFlatList } from "../components/HorizontalFlatList"; -import { LittleCard } from "../components/littleCard"; +import { MusicServiceProvider } from "../model/MusicServiceProvider"; +import { SimilarMusic } from "../components/SimilarMusicComponent"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import Artist from "../model/Artist"; +import { BlurView } from 'expo-blur'; const halfPi = Math.PI / 2; //@ts-ignore export default function DetailScreen({ route }) { - const music: Music = route.params.music; - const [currentspot] = useState(music); + const item: Music = route.params.music; const [simularMusic, setSimularMusic] = useState([]); + const [artistImage, setArtistImage] = useState(null); const [isPlaying, setIsPlaying] = useState(false); - const [sound, setSound] = useState(null); + const [addedToPlaylist, setAddedToPlaylist] = useState(false); + const [sound, setSound] = useState(); + const [loading, setLoading] = useState(true); const navigator = useNavigation(); - useEffect(() => { getSimilarTrack(); + getArtistImage(); + + if (item.trackPreviewUrl) { + loadMusic(); + } + + return () => { + if (sound) { + sound.unloadAsync(); + } + }; }, []); - const getSimilarTrack = async () => { - const simularMusic = await MusicServiceProvider.musicService.getSimilarTracks(currentspot.id); - setSimularMusic(simularMusic); - } - const handlePlaySound = async () => { - if (sound === null) { - const { sound: newSound } = await Audio.Sound.createAsync( - { uri: music.trackPreviewUrl }, - { shouldPlay: true } - ); - //setSound(newSound); - setIsPlaying(true); - - } else { - setIsPlaying(true); - //@ts-ignore - await sound.playAsync(); + const isFocused = useIsFocused(); + + useEffect(() => { + if (!isFocused && sound) { + sound.stopAsync(); + setIsPlaying(false); } + }, [isFocused]); + + const loadMusic = async () => { + const { sound } = await Audio.Sound.createAsync( + { uri: item.trackPreviewUrl }, + { shouldPlay: isPlaying }, + onPlaybackStatusUpdate + ); + setSound(sound); }; - const handleStopSound = async () => { - if (sound !== null) { + const getArtistImage = async () => { + const image = await MusicServiceProvider.musicService.getImageArtistWithId(item.artists[0].id); + setArtistImage(image); + }; + + const onPlaybackStatusUpdate = (status: any) => { + if (status.didJustFinish) { setIsPlaying(false); + } + }; - //@ts-ignore - await sound.stopAsync(); + const play = async () => { + if (sound) { + if (isPlaying) { + await sound.pauseAsync(); + } else { + await sound.replayAsync(); + await sound.playAsync(); + } + setIsPlaying(!isPlaying); } }; - useEffect(() => { - return sound ? () => { - console.log('Unloading Sound'); - //@ts-ignore - sound.unloadAsync(); + + const getSimilarTrack = async () => { + try { + const simularMusic = await MusicServiceProvider.musicService.getSimilarTracks(item.id); + setSimularMusic(simularMusic); + } finally { + setLoading(false); } - : undefined; - }, [sound]); + } const onShare = async () => { try { - const result = await Share.share({ + await Share.share({ message: - music.url, + item.url, }); } catch (error: any) { Alert.alert(error.message); @@ -77,11 +103,12 @@ export default function DetailScreen({ route }) { }; const addToPlaylist = async () => { - MusicServiceProvider.musicService.addToPlaylist(music.id); + MusicServiceProvider.musicService.addToPlaylist(item.id); + setAddedToPlaylist(true); }; const sensor = useAnimatedSensor(SensorType.ROTATION); - const styleAniamatedImage = useAnimatedStyle(() => { + const styleAnimatedImage = useAnimatedStyle(() => { const { pitch, roll } = sensor.sensor.value; const verticalAxis = interpolate( pitch, @@ -99,127 +126,254 @@ export default function DetailScreen({ route }) { }; }) + + const insets = useSafeAreaInsets(); + + const styles = StyleSheet.create({ + mainSafeArea: { + height: '100%', + width: '100%', + paddingTop: insets.top + }, + backgroundSection: { + height: "100%", + width: "100%", + position: "absolute" + }, + back_drop: { + height: "100%", + width: '100%', + position: "absolute", + }, + gradientFade: { + height: "100%", + }, + card: { + alignItems: 'center' + }, + cardCover: { + width: normalize(390), + height: normalize(390), + borderRadius: 16, + resizeMode: 'stretch' + }, + section1: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginHorizontal: "10%", + marginBottom: "4%", + marginTop: "5%" + }, + section2: { + flex: 1 + }, + section3: { + flexDirection: "row", + }, + similarTitle: { + color: "#FFF", + paddingLeft: "8%", + fontSize: normalize(28), + fontWeight: "600", + paddingBottom: normalize(15) + }, + title: { + maxWidth: "90%", + fontSize: normalize(30), + fontWeight: "bold", + color: "white" + }, + playButton: { + height: normalize(60), + width: normalize(60), + backgroundColor: "#E70E0E", + borderRadius: 30 + }, + imagePlayButton: { + width: normalize(40), + height: normalize(40) + }, + bodyPlayButton: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + artist: { + maxWidth: "40%", + fontWeight: "bold", + color: "white", + fontSize: normalize(17), + paddingLeft: normalize(5) + }, + date: { + fontWeight: "400", + color: "white", + fontSize: normalize(17) + }, + buttonArtist: { + flexDirection: "row", + alignItems: "center", + }, + saveButton: { + backgroundColor: '#3F3F3F', + width: normalize(180), + height: normalize(50), + padding: 10, + borderRadius: 8, + marginRight: normalize(10), + flexDirection: "row", + alignItems: "center" + }, + shareButton: { + backgroundColor: '#3F3F3F', + width: normalize(180), + height: normalize(50), + marginLeft: normalize(10), + padding: 10, + borderRadius: 8, + flexDirection: "row", + alignItems: "center" + }, + saveIcon: { + width: normalize(14.7), + height: normalize(21), + marginLeft: normalize(9) + }, + shareIcon: { + width: normalize(25), + height: normalize(25), + marginBottom: normalize(5) + }, + saveText: { + fontSize: normalize(11), + paddingLeft: normalize(9), + color: "white", + fontWeight: "bold" + }, + shareText: { + fontSize: normalize(11), + paddingLeft: normalize(5), + color: "white", + fontWeight: "bold" + }, + explicitImage: { + marginLeft: normalize(5), + width: normalize(16), + height: normalize(16) + }, + options: { + flexDirection: "row", + alignItems: "center", + paddingTop: normalize(15), + justifyContent: "center", + paddingHorizontal: normalize(20) + }, + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.1)', + }, + + }); + return ( - + + - - + /> + + + + - - + + + + + { Linking.openURL(item.url); }}> + + + - - - - - - - + + { Linking.openURL(item.url); }}> + {item.name} + {item.explicit && ( + - - - - + )} + + + { Linking.openURL(item.artists[0].url); }}> + {artistImage && ( + + )} + {item.artists.map((artist: Artist) => artist.name).join(', ')} + - {item.date} - {Math.floor(item.duration / 60)} min {Math.floor(item.duration % 60)} s + {item.trackPreviewUrl && ( + + + + + + )} - - - - - Dans ma collection - - - - {/* */} - Partager cette music + + + + + Dans ma collection + + + + + Partager cette music + + - {simularMusic.length !== 0 && ( - - {(props) => ( - { - // @ts-ignore - navigator.replace("Detail", { "music": props }) }} > - - - )} - - )} + + + + Similaire + {loading ? ( + + ) : + item.id} + renderItem={({ item }) => + { + // @ts-ignore + navigator.replace("Detail", { "music": item }) + }} > + + + } + />} + - + - ); -}; - -const styles = StyleSheet.create({ - mainSafeArea: { - flex: 1, - backgroundColor: "#141414", - }, - body: { - backgroundColor: "#0E0E0E" - }, - backgroundSection: { - height: "100%", - width: "100%", - position: "absolute" - }, - back_drop: { - height: "160%", - width: '430%', - position: "absolute", - }, - gradientFade: { - height: "100%", - }, - background1: { - height: '100%', - width: '100%', - }, - list: { - height: "100%" - }, - section1: { - paddingHorizontal: 25 - } -}) \ No newline at end of file +}; \ No newline at end of file diff --git a/src/FLAD/screens/FavoriteScreen.tsx b/src/FLAD/screens/FavoriteScreen.tsx index a149b46..cc177ca 100644 --- a/src/FLAD/screens/FavoriteScreen.tsx +++ b/src/FLAD/screens/FavoriteScreen.tsx @@ -1,18 +1,16 @@ import React, { useEffect } from 'react'; -import { StyleSheet, Text, View, FlatList, SafeAreaView } from 'react-native'; +import { StyleSheet, Text, View, FlatList, SafeAreaView, SectionList, TouchableOpacity } from 'react-native'; import CardMusic from '../components/CardMusicComponent'; import normalize from '../components/Normalize'; import { Svg, Path } from 'react-native-svg'; import FladyComponent from '../components/FladyComponent'; import { useNavigation } from "@react-navigation/native"; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; -import { useDispatch } from 'react-redux'; import { getFavoriteMusic } from '../redux/thunk/appThunk'; -import { Spot } from '../models/Spot'; -import { TouchableOpacity } from 'react-native-gesture-handler'; -import Artist from '../models/Artist'; +import { Spot } from '../model/Spot'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function FavoriteScreen() { @@ -34,14 +32,46 @@ export default function FavoriteScreen() { useEffect(() => { //@ts-ignore - dispatch(getFavoriteMusic()) + dispatch(getFavoriteMusic()); }, []); + const groupByDate = (data: Spot[]) => { + const groupedData: { [key: string]: Spot[] } = {}; + + const sortedData = data.sort((a, b) => b.date.getTime() - a.date.getTime()); + + sortedData.forEach((item) => { + const formattedDate = formatDate(item.date); + + if (groupedData[formattedDate]) { + groupedData[formattedDate].push(item); + } else { + groupedData[formattedDate] = [item]; + } + }); + + return Object.keys(groupedData).map((date) => ({ + title: date, + data: groupedData[date], + })); + }; + + const formatDate = (date: Date): string => { + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const year = date.getFullYear(); + + return `${day}/${month}/${year}`; + }; + + + const insets = useSafeAreaInsets(); const styles = StyleSheet.create({ mainSafeArea: { flex: 1, backgroundColor: style.body, + paddingTop: insets.top }, titleContainer: { marginVertical: 10, @@ -63,6 +93,13 @@ export default function FavoriteScreen() { fontSize: normalize(20), color: '#787878', marginBottom: 5 + }, + titleSection: { + fontSize: normalize(20), + color: style.Text, + fontWeight: 'medium', + marginLeft: 20, + marginBottom: 10 } }); @@ -78,15 +115,22 @@ export default function FavoriteScreen() { Retrouvez ici vos musiques favorites - item.music.id} renderItem={({ item }) => ( - //@ts-ignore - { navigation.navigate("Detail", { "music": item.music }) }}> - artist.name).join(', ')} id={item.music.id} /> + { + //@ts-ignore + navigation.navigate('Detail', { music: item.music }); + }}> + )} + renderSectionHeader={({ section: { title } }) => ( + //@ts-ignore + {title} + )} ListFooterComponent={ <> What's your mood? @@ -96,13 +140,12 @@ export default function FavoriteScreen() { keyExtractor={(item) => item.id.toString()} horizontal showsHorizontalScrollIndicator={false} - renderItem={({ item }) => ( - - )} + renderItem={({ item }) => } /> } nestedScrollEnabled={true} + /> ); diff --git a/src/FLAD/screens/ProfilScreen.tsx b/src/FLAD/screens/ProfilScreen.tsx index 79d97d7..229f0e1 100644 --- a/src/FLAD/screens/ProfilScreen.tsx +++ b/src/FLAD/screens/ProfilScreen.tsx @@ -11,8 +11,9 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; import { deleteUser } from '../redux/thunk/authThunk'; -import { setMail, setName } from '../redux/thunk/userThunk'; +import { setImage, setMail, setName, setPassword } from '../redux/thunk/userThunk'; import { setErrorUpdate } from '../redux/actions/userActions'; +import * as FileSystem from 'expo-file-system'; // @ts-ignore const DismissKeyboard = ({ children }) => ( @@ -32,6 +33,9 @@ export default function ProfilScreen() { const userCurrent = useSelector(state => state.userReducer.user); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); + const [oldPassword, setOldPassword] = React.useState(''); + const [newPassword, setNewPassword] = React.useState(''); + const [confirmPassword, setConfirmPassword] = React.useState(''); const style = isDark ? colorsDark : colorsLight; const navigation = useNavigation(); const [isModalVisible, setIsModalVisible] = React.useState(false); @@ -60,12 +64,25 @@ export default function ProfilScreen() { }; const pickImage = async () => { - await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.All, + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, - aspect: [4, 3], - quality: 1, + aspect: [3, 3], + quality: 0.2, }); + + if (result.assets !== null) { + const base64Image = await convertImageToBase64(result.assets[0].uri); + //@ts-ignore + dispatch(setImage(base64Image)); + } + }; + + const convertImageToBase64 = async (imageUri: any) => { + const base64 = await FileSystem.readAsStringAsync(imageUri, { + encoding: FileSystem.EncodingType.Base64, + }); + return `data:image/jpg;base64,${base64}`; }; const submitUsername = () => { @@ -134,6 +151,14 @@ export default function ProfilScreen() { ); } + const submitPassword = () => { + //@ts-ignore + dispatch(setPassword(oldPassword, newPassword)); + setOldPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } + useEffect(() => { if (errorUpdate) { Alert.alert( @@ -355,6 +380,7 @@ export default function ProfilScreen() { textInputConfirmModal: { marginLeft: 30, color: style.Text, + width: '67.5%', fontSize: normalize(18) }, textInputOldModal: { @@ -445,28 +471,35 @@ export default function ProfilScreen() { Mot de passe - + submitPassword()}> - Modifier + = 6 && newPassword === confirmPassword && oldPassword.length >= 6 ? '#1c77fb' : '#404040', + }]}>Modifier Ancien - + Nouveau - + Confirmer - + - Votre mot de passe doit comporter au moins 8 caractères, dont au moins un chiffre, une majuscule et une minuscule. + Votre mot de passe doit comporter au moins 6 caractères. diff --git a/src/FLAD/screens/SettingScreen.tsx b/src/FLAD/screens/SettingScreen.tsx index 9020662..ad23152 100644 --- a/src/FLAD/screens/SettingScreen.tsx +++ b/src/FLAD/screens/SettingScreen.tsx @@ -11,8 +11,8 @@ import { logout } from '../redux/thunk/authThunk'; import { darkMode } from '../redux/thunk/userThunk'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; -import { User } from '../models/User'; -import Artist from '../models/Artist'; +import { User } from '../model/User'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; // @ts-ignore const DismissKeyboard = ({ children }) => ( @@ -63,10 +63,15 @@ export default function SettingScreen() { const toggleLocalisation = () => setIsCheckedLocalisation(value => !value); + const insets = useSafeAreaInsets(); + + const styles = StyleSheet.create({ mainSafeArea: { flex: 1, backgroundColor: style.body, + paddingTop: insets.top + }, container: { marginTop: 30, @@ -92,7 +97,7 @@ export default function SettingScreen() { inputSearch: { placeholderTextColor: 'red', color: style.Text, - width: normalize(350), + width: "80%", }, profil: { paddingVertical: 9, @@ -336,7 +341,7 @@ export default function SettingScreen() { - artist.name).join(', ')} id='1' /> + @@ -353,14 +358,14 @@ export default function SettingScreen() { Se deconnecter - - Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - })} + + Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + })} diff --git a/src/FLAD/screens/SpotScreen.tsx b/src/FLAD/screens/SpotScreen.tsx index 77b4916..ef45228 100644 --- a/src/FLAD/screens/SpotScreen.tsx +++ b/src/FLAD/screens/SpotScreen.tsx @@ -9,9 +9,8 @@ import LottieView from 'lottie-react-native' import Lotties from '../assets/lottie/Lottie'; import Loading from '../components/LoadingComponent'; import { useNavigation } from '@react-navigation/native'; -import { addFavoritesMusic } from '../redux/actions/appActions'; import { useDispatch, useSelector } from 'react-redux'; -import { Spot } from '../models/Spot'; +import { Spot } from '../model/Spot'; import { removeFromSpotList, setSpotList } from '../redux/actions/spotActions'; export default function SpotScreen() { @@ -50,7 +49,7 @@ export default function SpotScreen() { function addLike(spot: Spot) { onLike(); - dispatch(addFavoritesMusic(spot.music)) + //dispatch(addFavoritesMusic(spot.music)) dispatch(removeFromSpotList(spot)); } function removeSpots(spot: Spot) { diff --git a/src/FLAD/services/musics/interfaces/IMusicService.ts b/src/FLAD/services/musics/interfaces/IMusicService.ts index 5f6cd72..d4c5134 100644 --- a/src/FLAD/services/musics/interfaces/IMusicService.ts +++ b/src/FLAD/services/musics/interfaces/IMusicService.ts @@ -1,4 +1,4 @@ -import Music from "../../../models/Music"; +import Music from "../../../model/Music"; export default interface IMusicService { getMusicById(id: string): Promise; @@ -8,4 +8,5 @@ export default interface IMusicService { getMusicsWithName(name: string): Promise; addToPlaylist(idTrack: string): void; getSimilarTracks(idTrack: string): Promise; + getImageArtistWithId(idArtist: string): Promise; } \ No newline at end of file diff --git a/src/FLAD/services/musics/spotify/SpotifyService.ts b/src/FLAD/services/musics/spotify/SpotifyService.ts index 74c8a57..f4e43e0 100644 --- a/src/FLAD/services/musics/spotify/SpotifyService.ts +++ b/src/FLAD/services/musics/spotify/SpotifyService.ts @@ -1,8 +1,8 @@ import axios from "axios"; -import Music from "../../../models/Music"; +import Music from "../../../model/Music"; import IMusicService from "../interfaces/IMusicService"; import TokenSpotify from "./TokenSpotify"; -import MusicMapper from "../../../models/mapper/MusicMapper"; +import MusicMapper from "../../../model/mapper/MusicMapper"; export default class SpotifyService implements IMusicService { private readonly API_URL = "https://api.spotify.com/v1"; @@ -186,4 +186,20 @@ export default class SpotifyService implements IMusicService { } } + async getImageArtistWithId(idArtist: string): Promise { + const access_token = await this._token.getAccessToken(); + + try { + const response = await axios.get(`${this.API_URL}/artists/${idArtist}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + return response.data.images[0].url; + } catch (error: any) { + console.log(error) + return null; + } + } + } \ No newline at end of file