New API route for changing password and functionality completion of Settings, Profile, Favorite and Detail pages
continuous-integration/drone/push Build is passing Details

pull/19/head
Emre KARTAL 1 year ago
parent 1a0e02bdef
commit f761028031

@ -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<Response | void> => {
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<Response | void> => {
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;

@ -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<void | Error> {
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<void | Error> {
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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

@ -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) {
<View style={styles.container}>
<View style={styles.imageContainer}>
<Image source={source} style={styles.image} />
<Image source={{ uri: props.music.cover }} style={styles.image} />
</View>
<View style={styles.textContainer}>
<Text style={[styles.title]}>{props.title}</Text>
<Text style={[styles.description]}>{props.description}</Text>
<Text style={[styles.title]}>{props.music.name}</Text>
<Text style={[styles.description]}>{props.music.artists.map((artist: Artist) => artist.name).join(', ')}</Text>
</View>
</View>
);

@ -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 (
<View style={styles.similarSection}>
<Text style={styles.similarTitle} >{title}</Text>
<FlatList
showsHorizontalScrollIndicator={false}
data={data}
horizontal={true}
keyExtractor={item => item.id}
renderItem={({ item }) => RenderCell(item)} /></View>
);
};
const styles = StyleSheet.create({
similarSection: {
paddingTop: 16
},
similarTitle: {
color: "#FFF",
paddingLeft: 8,
fontSize: 24,
fontWeight: "600",
paddingBottom: 16
}
});

@ -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 (
<View style={styles.similarContainer}>
<Image source={{ uri: props.music.cover }} style={styles.similarPoster}></Image>
<Text numberOfLines={1} style={styles.similarTitle}>{props.music.name}
</Text>
</View>
)
}
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
}
})

@ -1,31 +0,0 @@
import { View, Text, StyleSheet, Image } from 'react-native';
export interface RenderCellProps {
data : any;
}
export const LittleCard = (props: RenderCellProps) => {
return (
<View style={styles.similarContainer}>
<Image source={{ uri: props.data.cover }} style={styles.similarPoster}></Image>
<Text numberOfLines={2} style={styles.similarTitleFilm}>{props.data.name}
</Text>
</View>
)
}
const styles = StyleSheet.create({
similarContainer: {
marginHorizontal: 7
},
similarTitleFilm: {
color: "#DADADA",
paddingTop: 5,
fontWeight: "300"
},
similarPoster: {
height: 160,
width: 160,
borderRadius: 16
}
})

@ -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;
}

@ -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;
}

@ -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);
}
}

@ -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,

@ -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",

@ -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";
@ -16,10 +16,3 @@ export const setFavoriteMusic = (spots: Spot[]) => {
payload: spots,
};
}
export const addFavoritesMusic = (music: Music) => {
return {
type: favoritesTypes.ADD_FAVORITE_MUSICS,
payload: music,
};
}

@ -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[]) => {

@ -1,4 +1,4 @@
import { User } from "../../models/User";
import { User } from "../../model/User";
import { userTypes } from "../types/userTypes";
export interface LoginCredentials {

@ -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];

@ -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 = {

@ -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;

@ -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;
}

@ -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;
}

@ -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;
}
}

@ -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',
}

@ -1,3 +0,0 @@
export const playlistTypes = {
SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST',
}

@ -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,

@ -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<Music[]>([]);
const [artistImage, setArtistImage] = useState<string | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [sound, setSound] = useState(null);
const [addedToPlaylist, setAddedToPlaylist] = useState(false);
const [sound, setSound] = useState<Audio.Sound | null>();
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 getArtistImage = async () => {
const image = await MusicServiceProvider.musicService.getImageArtistWithId(item.artists[0].id);
setArtistImage(image);
};
const handleStopSound = async () => {
if (sound !== null) {
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 (
<View style={styles.body}>
<View>
<View style={styles.backgroundSection}>
<Image
blurRadius={133}
style={styles.back_drop}
source={{
uri: currentspot.cover,
uri: item.cover,
}}
></Image>
<LinearGradient style={styles.gradientFade}
colors={['rgba(56,56,56,0)', 'rgba(14,14,14,1)']}>
</LinearGradient>
/>
<View style={styles.overlay} />
<BlurView
style={styles.gradientFade}
intensity={70}
>
<LinearGradient
colors={['rgba(56,0,56,0)', 'rgba(14,14,14,1)']}
style={styles.gradientFade}
/>
</BlurView>
</View>
<View style={styles.background1}>
<ScrollView style={styles.list} showsVerticalScrollIndicator={false} scrollEventThrottle={4}>
<SafeAreaView style={styles.mainSafeArea}>
<ScrollView>
<View style={styles.card}>
<TouchableOpacity onPress={() => { Linking.openURL(item.url); }}>
<Animated.Image source={{ uri: item.cover }} style={[styles.cardCover, styleAnimatedImage]} />
</TouchableOpacity>
</View>
<View style={styles.section1}>
<View style={{ flex: 1, justifyContent: 'flex-start', alignItems: 'center' }}>
<View>
<Animated.Image
source={{
uri: currentspot.cover,
}}
style={[
{
width: normalize(429),
height: normalize(429),
borderRadius: 24,
resizeMode: 'stretch',
}, styleAniamatedImage
]}
/>
</View>
<View style={{ marginTop: 45, flex: 1, flexDirection: 'row', }}>
<View>
<View style={styles.section2}>
<TouchableOpacity style={{ flexDirection: "row", alignItems: "center" }} onPress={() => { Linking.openURL(item.url); }}>
<Text numberOfLines={1} style={styles.title}>{item.name}</Text>
{item.explicit && (
<Image style={styles.explicitImage} source={require('../assets/images/explicit_icon.png')} />
</View>
<TouchableOpacity activeOpacity={0.5} onPressIn={handlePlaySound}
onPressOut={handleStopSound} style={{
backgroundColor: '#F80404',
borderRadius: 100,
padding: normalize(23)
}}>
<View style={{ flex: 1, justifyContent: 'center', alignContent: 'center' }}>
</View>
)}
</TouchableOpacity>
<View style={styles.section3}>
<TouchableOpacity style={styles.buttonArtist} onPress={() => { Linking.openURL(item.artists[0].url); }}>
{artistImage && (
<Image style={{ width: normalize(30), height: normalize(30), borderRadius: 30 }} source={{ uri: artistImage }} />
)}
<Text numberOfLines={1} style={styles.artist}>{item.artists.map((artist: Artist) => artist.name).join(', ')}</Text>
<Text style={styles.date}> - {item.date} - {Math.floor(item.duration / 60)} min {Math.floor(item.duration % 60)} s</Text>
</TouchableOpacity>
</View>
</View>
{item.trackPreviewUrl && (
<TouchableOpacity style={styles.playButton} onPress={play}>
<View style={styles.bodyPlayButton}>
<Image style={styles.imagePlayButton} source={isPlaying ? require('../assets/images/play_icon.png') : require('../assets/images/pause_icon.png')} />
</View>
</TouchableOpacity>
)}
</View>
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-evenly', width: '100%' }}>
<TouchableOpacity onPress={addToPlaylist} activeOpacity={0.6} style={{
flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180,
height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606',
}}>
<Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Dans ma collection</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onShare} activeOpacity={0.6} style={{
flexDirection: 'row', justifyContent: 'space-evenly', alignItems: 'center', width: 180,
height: 64, borderRadius: 8, opacity: 0.86, backgroundColor: '#0B0606',
}}>
<Icon name="share" size={24} color="#FFFF"></Icon>
{/* <FontAwesome name="bookmark" size={24} color="#FF0000" ></FontAwesome> */}
<Text style={{ fontSize: normalize(16), fontWeight: "700", color: '#FFFFFF' }}>Partager cette music</Text>
<View style={styles.options}>
<TouchableOpacity onPress={addToPlaylist}>
<View style={styles.saveButton}>
<Image style={styles.saveIcon} source={addedToPlaylist ? require('../assets/images/save_icon_full.png') : require('../assets/images/save_icon.png')} />
<Text style={styles.saveText}>Dans ma collection</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={onShare}>
<View style={styles.shareButton}>
<Image style={styles.shareIcon} source={require('../assets/images/share_icon.png')} />
<Text style={styles.shareText}>Partager cette music</Text>
</View>
</TouchableOpacity>
</View>
{simularMusic.length !== 0 && (
<HorizontalFlatList title={'Similar'} data={simularMusic}>
{(props) => (
<Pressable
onPress={() => {
// @ts-ignore
navigator.replace("Detail", { "music": props }) }} >
<LittleCard data={props} />
</Pressable>
)}
</HorizontalFlatList>
)}
<View style={{ paddingTop: normalize(25) }}>
<Text style={styles.similarTitle} >Similaire</Text>
{loading ? (
<ActivityIndicator size="large" style={{ paddingTop: normalize(14) }} color="#FFF" />
) :
<FlatList
showsHorizontalScrollIndicator={false}
data={simularMusic}
horizontal={true}
keyExtractor={item => item.id}
renderItem={({ item }) =>
<TouchableOpacity
onPress={() => {
// @ts-ignore
navigator.replace("Detail", { "music": item })
}} >
<SimilarMusic music={item} />
</TouchableOpacity>
}
/>}
</View>
</ScrollView>
</View>
</SafeAreaView>
</View>
);
};
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
}
})

@ -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() {
</View>
<Text style={styles.description}>Retrouvez ici vos musiques favorites</Text>
</View>
<FlatList
data={favoriteMusic}
<SectionList
sections={groupByDate(favoriteMusic)}
keyExtractor={(item: Spot) => item.music.id}
renderItem={({ item }) => (
//@ts-ignore
<TouchableOpacity onPress={() => { navigation.navigate("Detail", { "music": item.music }) }}>
<CardMusic image={item.music.cover} title={item.music.name} description={item.music.artists.map((artist: Artist) => artist.name).join(', ')} id={item.music.id} />
<TouchableOpacity
onPress={() => {
//@ts-ignore
navigation.navigate('Detail', { music: item.music });
}}>
<CardMusic music={item.music} />
</TouchableOpacity>
)}
renderSectionHeader={({ section: { title } }) => (
//@ts-ignore
<Text style={styles.titleSection}>{title}</Text>
)}
ListFooterComponent={
<>
<Text style={[styles.title, { marginLeft: 20 }]}>What's your mood?</Text>
@ -96,13 +140,12 @@ export default function FavoriteScreen() {
keyExtractor={(item) => item.id.toString()}
horizontal
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => (
<FladyComponent image={item.source} />
)}
renderItem={({ item }) => <FladyComponent image={item.source} />}
/>
</>
}
nestedScrollEnabled={true}
/>
</SafeAreaView>
);

@ -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() {
</View>
</TouchableOpacity>
<Text style={styles.titlePassword}>Mot de passe</Text>
<TouchableOpacity>
<TouchableOpacity
disabled={newPassword.length < 6 || newPassword !== confirmPassword || oldPassword.length < 6}
onPress={() => submitPassword()}>
<View>
<Text style={styles.updateText}>Modifier</Text>
<Text style={[styles.updateText, {
color: newPassword.length >= 6 && newPassword === confirmPassword && oldPassword.length >= 6 ? '#1c77fb' : '#404040',
}]}>Modifier</Text>
</View>
</TouchableOpacity>
</View>
<View style={styles.bodyModal}>
<View style={styles.optionModalWithUnderline}>
<Text style={styles.textOptionModal}>Ancien</Text>
<TextInput placeholderTextColor='#828288' placeholder="saisir l'ancien mot de passe" style={styles.textInputOldModal} />
<TextInput placeholderTextColor='#828288' value={oldPassword} secureTextEntry={true}
onChangeText={setOldPassword} placeholder="saisir l'ancien mot de passe" style={styles.textInputOldModal} />
</View>
<View style={styles.optionModalWithUnderline}>
<Text style={styles.textOptionModal}>Nouveau</Text>
<TextInput placeholderTextColor='#828288' placeholder='saisir le mot de passe' style={styles.textInputNewModal} />
<TextInput placeholderTextColor='#828288' value={newPassword} secureTextEntry={true}
onChangeText={setNewPassword} placeholder='saisir le mot de passe' style={styles.textInputNewModal} />
</View>
<View style={styles.optionModal}>
<Text style={styles.textOptionModal}>Confirmer</Text>
<TextInput placeholderTextColor='#828288' placeholder='mot de passe' style={styles.textInputConfirmModal} />
<TextInput placeholderTextColor='#828288' value={confirmPassword} secureTextEntry={true}
onChangeText={setConfirmPassword} placeholder='mot de passe' style={styles.textInputConfirmModal} />
</View>
</View>
<View style={styles.warningView}>
<Text style={styles.warning}>Votre mot de passe doit comporter au moins 8 caractères, dont au moins un chiffre, une majuscule et une minuscule.</Text>
<Text style={styles.warning}>Votre mot de passe doit comporter au moins 6 caractères.</Text>
</View>
</View>
</Modal>

@ -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() {
</View>
<View style={styles.musicActually}>
<CardMusic image={currentMusic.cover} title={currentMusic.name} description={currentMusic.artists.map((artist: Artist) => artist.name).join(', ')} id='1' />
<CardMusic music={currentMusic} />
<Image source={require("../assets/images/flady_icon.png")} style={styles.mascot} />
</View>
</>
@ -353,14 +358,14 @@ export default function SettingScreen() {
<Text style={styles.textDeconnectionOption}>Se deconnecter</Text>
</TouchableOpacity>
</View>
<View style={{alignItems: 'center'}}>
<Text style={styles.creationDateText}>Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
})}</Text>
<View style={{ alignItems: 'center' }}>
<Text style={styles.creationDateText}>Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
})}</Text>
</View>
</View>
</ScrollView>

@ -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) {

@ -1,4 +1,4 @@
import Music from "../../../models/Music";
import Music from "../../../model/Music";
export default interface IMusicService {
getMusicById(id: string): Promise<Music>;
@ -8,4 +8,5 @@ export default interface IMusicService {
getMusicsWithName(name: string): Promise<Music[]>;
addToPlaylist(idTrack: string): void;
getSimilarTracks(idTrack: string): Promise<Music[]>;
getImageArtistWithId(idArtist: string): Promise<string | null>;
}

@ -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<string | null> {
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;
}
}
}
Loading…
Cancel
Save