Compare commits
118 Commits
@ -0,0 +1,19 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: sonar-analyses
|
||||||
|
image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
|
||||||
|
commands:
|
||||||
|
- npm install
|
||||||
|
- npm run test
|
||||||
|
- ls ./components/__tests__/
|
||||||
|
- sonar-scanner -Dsonar.projectKey=MovieFinder -Dsonar.sources=. -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
|
||||||
|
-Dsonar.login=$${PLUGIN_SONAR_TOKEN} -Dsonar.javascript.lcov.reportPaths=./components/__tests__/lcov.info
|
||||||
|
-Dsonar.exclusions=**/lcov-report/**
|
||||||
|
secrets: [ SONAR_TOKEN ]
|
||||||
|
settings:
|
||||||
|
sonar_host: https://codefirst.iut.uca.fr/sonar/
|
||||||
|
sonar_token:
|
||||||
|
from_secret: SONAR_TOKEN
|
@ -1,22 +1,25 @@
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import {StatusBar} from 'expo-status-bar'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||||
import useCachedResources from './hooks/useCachedResources';
|
import useCachedResources from './hooks/useCachedResources'
|
||||||
import useColorScheme from './hooks/useColorScheme';
|
import useColorScheme from './hooks/useColorScheme'
|
||||||
import Navigation from './navigation';
|
import Navigation from './navigation'
|
||||||
import {View} from "react-native";
|
import store from "./redux/store"
|
||||||
|
import {Provider} from "react-redux"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const isLoadingComplete = useCachedResources();
|
const isLoadingComplete = useCachedResources()
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme()
|
||||||
|
|
||||||
if (!isLoadingComplete) {
|
if (!isLoadingComplete) {
|
||||||
return null;
|
return null
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<Provider store={store}>
|
||||||
<Navigation colorScheme={colorScheme} />
|
<SafeAreaProvider>
|
||||||
<StatusBar />
|
<Navigation colorScheme={colorScheme}/>
|
||||||
</SafeAreaProvider>
|
<StatusBar/>
|
||||||
);
|
</SafeAreaProvider>
|
||||||
}
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 2.3 MiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 2.6 MiB |
After Width: | Height: | Size: 2.1 MiB |
@ -0,0 +1,89 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://codefirst.iut.uca.fr/git/lucas.delanier/MovieFinder/raw/branch/master/Documentation/banner_image.png " />
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
</br>
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
[](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
|
||||||
|
|
||||||
|
|
||||||
|
**MovieFinder** est une application mobile qui permet de découvrir des films. Elle propose chaque jour une liste contenant les 20 films les plus tendances du jour. Une fois la liste vidée, vous êtes invités à attendre la fin du décompte, qui est fixé à minuit, afin d'obtenir une nouvelle liste de films journaliers.
|
||||||
|
|
||||||
|
## :floppy_disk: FEATURES
|
||||||
|
|
||||||
|
Sur la page principale, vous pouvez swipe les cartes pour passer d'un film à l'autre sans faire d'action sur ceux-ci. Pour les faire disparaître de la pile, vous pouvez utiliser les boutons "Watch Later" et "Favourite", respectivement à gauche et à droite, pour les ajouter à la liste correspondante. Vous pouvez également utiliser le bouton "Supprimer" qui est situé au milieu et qui permet de supprimer le film de la pile sans l'affecter à aucune des deux listes citées précédemment. Une fois la liste vide vous devez attendre jusqu'a la fin du décompte fixé a 00:00 pour avir de nouveaux films.
|
||||||
|
|
||||||
|
Les listes "WatchLater" et "Favourite" affichent les informations principales des films présents dans les listes. Pour accéder aux informations complètes, il suffit d'appuyer sur le film pour être redirigé sur la page "Info" correspondant au film selectionné.
|
||||||
|
|
||||||
|
La page "Info" permet de visualiser toutes les informations d'un film (titre, note moyenne, durée, date de sortie, genres, synopsis, bande annonce, casting, commentaires, films similaires).
|
||||||
|
|
||||||
|
Deux thèmes différents sont disponibles : sombre et clair. Ils sont choisis en fonction du thème selectionné sur le téléphone de l'utilisateur. (change uniquement la navigationbar pour le moment pour un soucs d'hestétique)
|
||||||
|
|
||||||
|
L'API utilisée est : The Movie DataBase (TMDB) API : https://developers.themoviedb.org/3</br>
|
||||||
|
Pour afficher nos "Coup de coeur" nous avons développé notre propre API que nous mettrons à jour en fonction de nos propres gouts. disponible ici : https://codefirst.iut.uca.fr/git/lucas.delanier/moviefinder_api
|
||||||
|

|
||||||
|
|
||||||
|
Plus d'informations sont disponibles dans le dossier Documentation comme un schéma d'architecture globale expliquant l'appel a l'API ainsi que du localstorage.
|
||||||
|
|
||||||
|
## :dizzy: Getting Started
|
||||||
|
|
||||||
|
Une fois le dépot cloné, vous pouvez lancer le code sur votre téléphone Android et IOS grace a l'outil [Expo](https://docs.expo.dev/get-started/installation/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx expo start
|
||||||
|
```
|
||||||
|
Une fois la commande executée, il vous suffit de scanner le QR à partir de l'application Expo sur android et Caméra pour IOS.</br>
|
||||||
|
|
||||||
|
**/!\ Veuillez faire attention à bien etre connecté sur le même réseau (ordinateaur et téléphone).**
|
||||||
|
|
||||||
|
Si vous rencontrez des problèmes liés aux "RNSVGSvgViewAndroid", utilisez cette commande :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx expo install react-native-svg
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## :gift: OverView
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://codefirst.iut.uca.fr/git/lucas.delanier/MovieFinder/raw/branch/master/Documentation/exemplebackground.png" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Les sketchs de l'application sont disponibles sur le figma suivant:
|
||||||
|
[Figma MovieFinder](https://www.figma.com/file/dbTIviWlglo4boYu1hTJ1e/MovieFinder?node-id=0%3A1&t=ls9V1qC8pOlnEeY1-1)
|
||||||
|
|
||||||
|
## :wrench: SUPPORT
|
||||||
|
En cas de problème lors de l'utilisation de l'application, vous pouvez nous contacer aux adresses suivantes :
|
||||||
|
|
||||||
|
|
||||||
|
Lucas Delanier : **lucas.delanier@etu.uca.fr** </br>
|
||||||
|
Louison Parant : **louison.parant@etu.uca.fr**
|
||||||
|

|
||||||
|
|
||||||
|
## ✨ Contributors
|
||||||
|
|
||||||
|
<a href = "https://codefirst.iut.uca.fr/git/lucas.delanier">
|
||||||
|
<img src ="https://codefirst.iut.uca.fr/git/avatars/6a3835d734392fccff3949f7c82a63b9?size=870" height="50px">
|
||||||
|
</a>
|
||||||
|
<a href = "https://codefirst.iut.uca.fr/git/louison.parant">
|
||||||
|
<img src ="https://codefirst.iut.uca.fr/git/avatars/b337a607f680a2d9af25eb09ea457be9?size=870" height="50px">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 603 KiB |
After Width: | Height: | Size: 2.4 MiB |
After Width: | Height: | Size: 796 KiB |
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 561 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 2.6 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 978 B |
After Width: | Height: | Size: 3.6 KiB |
@ -1,6 +1,9 @@
|
|||||||
module.exports = function(api) {
|
module.exports = function (api) {
|
||||||
api.cache(true);
|
api.cache(true);
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo']
|
presets: ['babel-preset-expo'],
|
||||||
};
|
plugins: [
|
||||||
|
'react-native-reanimated/plugin',
|
||||||
|
]
|
||||||
|
};
|
||||||
};
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
import {StyleSheet, Text, View} from "react-native";
|
||||||
|
import * as React from "react";
|
||||||
|
import Stars from "./StarsComponent";
|
||||||
|
import Movie from "../model/Movie";
|
||||||
|
import {formatTime} from "../model/formatTime";
|
||||||
|
|
||||||
|
type headerMovieProps = {
|
||||||
|
movie: Movie
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderMovie(props: headerMovieProps) {
|
||||||
|
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
circle: {
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 100 / 2,
|
||||||
|
marginTop: 4,
|
||||||
|
backgroundColor: "lightgray",
|
||||||
|
marginHorizontal: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<View style={{flexDirection: 'column', alignSelf: 'center', paddingHorizontal: 30, width: '100%', alignItems: "center", paddingTop: 10, flex: 0.07}}>
|
||||||
|
<Text numberOfLines={1} style={{color: "white", fontSize: 30, fontWeight: "bold", paddingTop: 5, alignSelf: "center"}}>{props.movie.original_title}</Text>
|
||||||
|
<View style={{flexDirection: 'row', justifyContent: "center", alignItems: "center", alignSelf: "center"}}>
|
||||||
|
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${props.movie.release_date}`}</Text>
|
||||||
|
<View style={styles.circle}/>
|
||||||
|
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${props.movie.genres[0]} ${props.movie.genres[1] !== undefined ? ", " + props.movie.genres[1] : ""}`}</Text>
|
||||||
|
<View style={styles.circle}/>
|
||||||
|
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${formatTime(props.movie.runtime)}`}</Text>
|
||||||
|
</View>
|
||||||
|
<Stars note={props.movie.vote_average} size={110}/>
|
||||||
|
</View>);
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import Movie from "../model/Movie.js";
|
||||||
|
import {Image, StyleSheet, Text, View} from "react-native";
|
||||||
|
import {LinearGradient} from "expo-linear-gradient";
|
||||||
|
import Stars from "./StarsComponent";
|
||||||
|
import * as React from "react";
|
||||||
|
import {formatTime} from "../model/formatTime";
|
||||||
|
|
||||||
|
type MovieListProps = {
|
||||||
|
movie: Movie
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MovieListComponent(props: MovieListProps) {
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
filmCard: {
|
||||||
|
width: 70,
|
||||||
|
height: 110,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
height: 130,
|
||||||
|
|
||||||
|
borderRadius: 20,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginVertical: 7,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
backgroundColor: "#1D1D1D",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: "#1F1F1F"
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
height: 130,
|
||||||
|
width: "85%",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexDirection: 'column',
|
||||||
|
paddingRight: 20,
|
||||||
|
paddingLeft: 20,
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 21,
|
||||||
|
},
|
||||||
|
infoSection: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%"
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
color: "grey",
|
||||||
|
fontWeight: "600"
|
||||||
|
},
|
||||||
|
vote: {
|
||||||
|
paddingLeft: 7,
|
||||||
|
color: "white",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: 13
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<LinearGradient style={styles.body} start={{x: 0, y: 1}}
|
||||||
|
end={{x: 1, y: 1}}
|
||||||
|
// Button Linear Gradient
|
||||||
|
colors={['#0B0B0B', '#1F1F1F']}>
|
||||||
|
|
||||||
|
|
||||||
|
<Image
|
||||||
|
style={styles.filmCard}
|
||||||
|
source={{
|
||||||
|
uri: props.movie.poster_path_min,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text numberOfLines={1} style={styles.h1}>{props.movie.original_title}</Text>
|
||||||
|
<View style={styles.infoSection}>
|
||||||
|
<View style={styles.top}>
|
||||||
|
<Stars note={props.movie.vote_average} size={90}></Stars>
|
||||||
|
<Text style={styles.vote}>{props.movie.vote_average.toFixed(1)}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.h3}>{formatTime(props.movie.runtime)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text numberOfLines={3} style={{color: "#C7C7C7", fontWeight: "600",}}>{props.movie.overview}</Text>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import {Image, View} from "react-native";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
type StarsProps = {
|
||||||
|
note: number
|
||||||
|
size: number
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stars(props: StarsProps) {
|
||||||
|
let imageSource;
|
||||||
|
let note = props.note / 2;
|
||||||
|
if (note < 0.5)
|
||||||
|
imageSource = require('../assets/images/0.5stars_vote.png');
|
||||||
|
else if (note < 1)
|
||||||
|
imageSource = require('../assets/images/1stars_vote.png');
|
||||||
|
else if (note < 1.5)
|
||||||
|
imageSource = require('../assets/images/1.5stars_vote.png');
|
||||||
|
else if (note < 2)
|
||||||
|
imageSource = require('../assets/images/2stars_vote.png');
|
||||||
|
else if (note < 2.5)
|
||||||
|
imageSource = require('../assets/images/2.5stars_vote.png');
|
||||||
|
else if (note < 3)
|
||||||
|
imageSource = require('../assets/images/3stars_vote.png');
|
||||||
|
else if (note < 3.5)
|
||||||
|
imageSource = require('../assets/images/3.5stars_vote.png');
|
||||||
|
else if (note < 4)
|
||||||
|
imageSource = require('../assets/images/4stars_vote.png');
|
||||||
|
else if (note < 4.5)
|
||||||
|
imageSource = require('../assets/images/4.5stars_vote.png');
|
||||||
|
else if (note < 5)
|
||||||
|
imageSource = require('../assets/images/5stars_vote.png');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Image source={imageSource} style={{
|
||||||
|
width: props.size,
|
||||||
|
height: 40,
|
||||||
|
resizeMode: 'contain'
|
||||||
|
}}/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Stars;
|
@ -0,0 +1,44 @@
|
|||||||
|
import {Image, Text, View} from "react-native";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type TimerProps = {
|
||||||
|
hours: number
|
||||||
|
minutes: number
|
||||||
|
seconds: number
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer(props: TimerProps) {
|
||||||
|
return (
|
||||||
|
<View style={{zIndex: 1, alignContent: "center", justifyContent: "center", flex: 0.15, flexDirection: "row"}}>
|
||||||
|
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>Nouvelle collection dans</Text>
|
||||||
|
<Image source={require('../assets/images/timer_icon.png')} style={{
|
||||||
|
height: 30,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
top: -5
|
||||||
|
}}></Image>
|
||||||
|
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.hours.toString().padStart(2, '0')}:`}</Text>
|
||||||
|
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.minutes.toString().padStart(2, '0')}:`}</Text>
|
||||||
|
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.seconds.toString().padStart(2, '0')}`}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Timer2(props: TimerProps) {
|
||||||
|
return (
|
||||||
|
<View style={{zIndex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 20, paddingVertical: 10, borderRadius: 100, flexDirection: "row", bottom: 0, backgroundColor: "white", marginTop: 50}}>
|
||||||
|
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>Nouvelle collection dans</Text>
|
||||||
|
<Image source={require('../assets/images/timer_icon2.png')} style={{
|
||||||
|
height: 30,
|
||||||
|
resizeMode: 'contain', marginHorizontal: 7
|
||||||
|
|
||||||
|
}}></Image>
|
||||||
|
|
||||||
|
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.hours.toString().padStart(2, '0')}:`}</Text>
|
||||||
|
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.minutes.toString().padStart(2, '0')}:`}</Text>
|
||||||
|
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.seconds.toString().padStart(2, '0')}`}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import { MonoText } from '../StyledText';
|
|
||||||
|
|
||||||
it(`renders correctly`, () => {
|
|
||||||
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
@ -0,0 +1,216 @@
|
|||||||
|
import Movie from "../../model/Movie"
|
||||||
|
import {describe, expect} from '@jest/globals'
|
||||||
|
import {addMovieToFavourite, addMovieToWatchLater, removeMovieTrending, setFavourite, setinfoMovie, setWatchLater} from "../../redux/actions/actions"
|
||||||
|
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from "../../redux/constants"
|
||||||
|
|
||||||
|
describe('test actions add WatchLater', () => {
|
||||||
|
|
||||||
|
it('should create an action with ADD_WATCHLATER type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: ADD_WATCHLATER,
|
||||||
|
payload: payload
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(addMovieToWatchLater(payload)).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test actions add Favourite', () => {
|
||||||
|
|
||||||
|
it('should create an action with ADD_FAVOURITE type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: ADD_FAVOURITE,
|
||||||
|
payload: payload
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(addMovieToFavourite(payload)).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('test actions load watchlater', () => {
|
||||||
|
|
||||||
|
it('should create an action with ADD_FAVOURITE type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: LOAD_WATCHLATER,
|
||||||
|
payload: [payload]
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(setWatchLater([payload])).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test actions load favourite', () => {
|
||||||
|
|
||||||
|
it('should create an action with LOAD_FAVOURITE type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: LOAD_FAVOURITE,
|
||||||
|
payload: [payload]
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(setFavourite([payload])).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('test actions load info movies', () => {
|
||||||
|
|
||||||
|
it('should create an action with FETCH_TRENDING_MOVIE type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: FETCH_TRENDING_MOVIE,
|
||||||
|
payload: [payload]
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(setinfoMovie([payload])).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('test actions remove movies', () => {
|
||||||
|
|
||||||
|
it('should create an action with removeMovieTrending type', () => {
|
||||||
|
const payload = new Movie(
|
||||||
|
916224,
|
||||||
|
"Suzume",
|
||||||
|
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
|
||||||
|
121,
|
||||||
|
8.311,
|
||||||
|
"2022-11-11",
|
||||||
|
[
|
||||||
|
"Animation",
|
||||||
|
"Drame",
|
||||||
|
"Aventure",
|
||||||
|
"Fantastique",
|
||||||
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
|
||||||
|
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
const expectation = {
|
||||||
|
type: POP_FIRST_TRENDING,
|
||||||
|
payload: payload
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(removeMovieTrending(payload)).toEqual(expectation);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
|||||||
|
import Movie from "../../model/Movie"
|
||||||
|
import {describe, expect} from '@jest/globals'
|
||||||
|
import appReducer from "../../redux/reducers/appReducer"
|
||||||
|
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from "../../redux/constants"
|
||||||
|
|
||||||
|
describe('test null', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return initial state', () => {
|
||||||
|
expect(appReducer(undefined, {})).toEqual(initialState)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test ADD_FAVOURITE', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle ADD_FAVOURITE', () => {
|
||||||
|
const favourite = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: ADD_FAVOURITE,
|
||||||
|
payload: favourite,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [favourite, ...initialState.favouriteMovies],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test ADD_WATCHLATER', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle ADD_WATCHLATER', () => {
|
||||||
|
const watchLater = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: ADD_WATCHLATER,
|
||||||
|
payload: watchLater,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [watchLater, ...initialState.watchLaterMovies],
|
||||||
|
favouriteMovies: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test LOAD_WATCHLATER', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle LOAD_WATCHLATER', () => {
|
||||||
|
const watchLater = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
const MovieList = [watchLater]
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: LOAD_WATCHLATER,
|
||||||
|
payload: MovieList,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [watchLater, ...initialState.watchLaterMovies],
|
||||||
|
favouriteMovies: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test LOAD_FAVOURITE', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle LOAD_FAVOURITE', () => {
|
||||||
|
const favourite = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
const MovieList = [favourite]
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: LOAD_FAVOURITE,
|
||||||
|
payload: MovieList,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [favourite, ...initialState.favouriteMovies],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test FETCH_TRENDING_MOVIE', () => {
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle FETCH_TRENDING_MOVIE', () => {
|
||||||
|
const trending = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
const MovieList = [trending]
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: FETCH_TRENDING_MOVIE,
|
||||||
|
payload: MovieList,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [trending, ...initialState.trendingMovies],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('test POP_FIRST_TRENDING', () => {
|
||||||
|
|
||||||
|
const trending = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
|
||||||
|
|
||||||
|
let initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [trending],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should handle POP_FIRST_TRENDING', () => {
|
||||||
|
expect(
|
||||||
|
appReducer(initialState, {
|
||||||
|
type: POP_FIRST_TRENDING,
|
||||||
|
payload: trending,
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [...initialState.trendingMovies.filter((item: Movie) => item !== trending)],
|
||||||
|
watchLaterMovies: [],
|
||||||
|
favouriteMovies: [],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,32 @@
|
|||||||
|
import {describe, expect, test} from '@jest/globals';
|
||||||
|
import {formatTime} from "../../model/formatTime";
|
||||||
|
|
||||||
|
describe('return formated time', () => {
|
||||||
|
test('125to 2h 05m', () => {
|
||||||
|
expect(formatTime(125)).toBe("2h 05m");
|
||||||
|
});
|
||||||
|
test('45to 0h 45m', () => {
|
||||||
|
expect(formatTime(45)).toBe("0h 45m");
|
||||||
|
});
|
||||||
|
test('203to 3h 23m', () => {
|
||||||
|
expect(formatTime(203)).toBe("3h 23m");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
"verbose": true,
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"**.test.js"
|
||||||
|
],
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testEnvironmentOptions": {
|
||||||
|
"browsers": [
|
||||||
|
"chrome",
|
||||||
|
"firefox",
|
||||||
|
"safari"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
*/
|
@ -0,0 +1,27 @@
|
|||||||
|
import {Image, View} from "react-native";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function SuggestedCard() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Image style={{height: 28, width: 152, marginVertical: 5}}
|
||||||
|
source={require('../assets/images/suggested_card.png')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewCard() {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Image style={{height: 28, width: 99, marginVertical: 5}}
|
||||||
|
source={require('../assets/images/new_card.png')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
api_key: "a133422b5b1f22428e8074470d321865",
|
||||||
|
base_url: "https://api.themoviedb.org/3/"
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||||
|
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
|
||||||
|
);
|
@ -0,0 +1,16 @@
|
|||||||
|
class MinimalMovie {
|
||||||
|
public original_title: string
|
||||||
|
|
||||||
|
public poster_path: string
|
||||||
|
|
||||||
|
|
||||||
|
constructor(original_title: string, poster_path: string) {
|
||||||
|
this.original_title = original_title;
|
||||||
|
this.poster_path = 'https://image.tmdb.org/t/p/w185' + poster_path;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MinimalMovie;
|
@ -0,0 +1,33 @@
|
|||||||
|
class Movie {
|
||||||
|
|
||||||
|
public poster_path_min : string
|
||||||
|
|
||||||
|
public full_date : string
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public original_title: string,
|
||||||
|
public poster_path: string,
|
||||||
|
public runtime: number,
|
||||||
|
public vote_average: number,
|
||||||
|
public release_date: string,
|
||||||
|
public genres: string[],
|
||||||
|
public overview: string,
|
||||||
|
public backdrop_path: string
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.original_title = original_title;
|
||||||
|
this.poster_path = 'https://image.tmdb.org/t/p/w780' + poster_path;
|
||||||
|
this.poster_path_min = 'https://image.tmdb.org/t/p/w185' + poster_path;
|
||||||
|
this.runtime = runtime;
|
||||||
|
this.release_date = release_date.substring(0, 4);
|
||||||
|
this.full_date = release_date;
|
||||||
|
this.genres = genres;
|
||||||
|
this.overview = overview;
|
||||||
|
this.vote_average = vote_average;
|
||||||
|
this.backdrop_path = 'https://image.tmdb.org/t/p/original' + backdrop_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Movie;
|
@ -0,0 +1,6 @@
|
|||||||
|
export function formatTime(time: number) {
|
||||||
|
const hours = Math.floor(time / 60);
|
||||||
|
const minutes = time % 60;
|
||||||
|
const minutesToDisplay = minutes < 10 ? `0${minutes}` : minutes
|
||||||
|
return `${hours}h ${minutesToDisplay}m`;
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
class Review {
|
||||||
|
public message: string
|
||||||
|
|
||||||
|
public pseudo: string
|
||||||
|
|
||||||
|
public profile_path: string
|
||||||
|
|
||||||
|
public date: string
|
||||||
|
|
||||||
|
|
||||||
|
constructor(message: string, profile_path: string, date: string, pseudo: string) {
|
||||||
|
this.message = message;
|
||||||
|
if (profile_path == null) {
|
||||||
|
this.profile_path = "https://thumbs.dreamstime.com/b/profil-vectoriel-avatar-par-d%C3%A9faut-utilisateur-179376714.jpg";
|
||||||
|
} else if (profile_path.slice(0, 6) === "/https") {
|
||||||
|
this.profile_path = profile_path.slice(1, 100);
|
||||||
|
} else {
|
||||||
|
this.profile_path = 'https://image.tmdb.org/t/p/w185' + profile_path;
|
||||||
|
}
|
||||||
|
this.date = date.substring(0, 10);
|
||||||
|
this.pseudo = pseudo;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Review;
|
@ -1,110 +1,146 @@
|
|||||||
/**
|
import {FontAwesomeIcon} from "@fortawesome/react-native-fontawesome"
|
||||||
* If you are not familiar with React Navigation, refer to the "Fundamentals" guide:
|
import {faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons"
|
||||||
* https://reactnavigation.org/docs/getting-started
|
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
|
||||||
*
|
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native'
|
||||||
*/
|
import {createNativeStackNavigator} from '@react-navigation/native-stack'
|
||||||
import { FontAwesome } from '@expo/vector-icons';
|
import * as React from 'react'
|
||||||
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
|
import {ColorSchemeName} from 'react-native'
|
||||||
import { faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons";
|
import useColorScheme from '../hooks/useColorScheme'
|
||||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
import NotFoundScreen from '../screens/NotFoundScreen'
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import WatchLaterScreen from '../screens/WatchLaterScreen'
|
||||||
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
|
import FavoriteScreen from '../screens/FavoriteScreen'
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import HomeScreen from '../screens/HomeScreen'
|
||||||
import * as React from 'react';
|
import InfoScreen from '../screens/InfoScreen'
|
||||||
import { ColorSchemeName, Pressable } from 'react-native';
|
import {RootStackParamList, RootTabParamList, RootTabScreenProps} from '../types'
|
||||||
|
import LinkingConfiguration from './LinkingConfiguration'
|
||||||
import Colors from '../constants/Colors';
|
import {useCallback, useEffect, useState} from "react"
|
||||||
import useColorScheme from '../hooks/useColorScheme';
|
import {useDispatch} from "react-redux"
|
||||||
import NotFoundScreen from '../screens/NotFoundScreen';
|
import {getTrendingID, loadWatchLater} from "../redux/actions/actions"
|
||||||
import WatchLaterScreen from '../screens/WatchLaterScreen';
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import FavoriteScreen from '../screens/FavoriteScreen';
|
|
||||||
import HomeScreen from '../screens/HomeScreen';
|
export default function Navigation({colorScheme}: { colorScheme: ColorSchemeName }) {
|
||||||
import { RootStackParamList, RootTabParamList, RootTabScreenProps } from '../types';
|
const [appIsReady, setAppIsReady] = useState(false)
|
||||||
import LinkingConfiguration from './LinkingConfiguration';
|
const dispatch = useDispatch()
|
||||||
|
useEffect(() => {
|
||||||
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
|
|
||||||
return (
|
async function prepare() {
|
||||||
<NavigationContainer
|
try {
|
||||||
linking={LinkingConfiguration}
|
const loadTrendingID = async () => {
|
||||||
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
// @ts-ignore
|
||||||
<RootNavigator />
|
dispatch(getTrendingID())
|
||||||
</NavigationContainer>
|
};
|
||||||
);
|
loadTrendingID()
|
||||||
|
|
||||||
|
const list = dispatch(loadWatchLater())
|
||||||
|
loadWatchLater(list)
|
||||||
|
} catch (e) {
|
||||||
|
} finally {
|
||||||
|
// Tell the application to render
|
||||||
|
setAppIsReady(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
prepare();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useCallback(async () => {
|
||||||
|
if (appIsReady) {
|
||||||
|
// This tells the splash screen to hide immediately! If we call this after
|
||||||
|
// `setAppIsReady`, then we may see a blank screen while the app is
|
||||||
|
// loading its initial state and rendering its first pixels. So instead,
|
||||||
|
// we hide the splash screen once we know the root view has already
|
||||||
|
// performed layout.
|
||||||
|
await SplashScreen.hideAsync()
|
||||||
|
}
|
||||||
|
}, [appIsReady]);
|
||||||
|
if (!appIsReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<NavigationContainer
|
||||||
|
linking={LinkingConfiguration}
|
||||||
|
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||||
|
<RootNavigator/>
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A root stack navigator is often used for displaying modals on top of all other content.
|
* A root stack navigator is often used for displaying modals on top of all other content.
|
||||||
* https://reactnavigation.org/docs/modal
|
* https://reactnavigation.org/docs/modal
|
||||||
*/
|
*/
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>()
|
||||||
|
|
||||||
function RootNavigator() {
|
function RootNavigator() {
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
<Stack.Screen name="Root" component={BottomTabNavigator} options={{ headerShown: false }} />
|
<Stack.Screen name="Root" component={BottomTabNavigator} options={{headerShown: false}}/>
|
||||||
<Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="Home" component={HomeScreen} options={{headerShown: false}}/>
|
||||||
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
|
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{title: 'Oops!'}}/>
|
||||||
<Stack.Screen name="Favorite" component={FavoriteScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="Favorite" component={FavoriteScreen} options={{headerShown: false}}/>
|
||||||
<Stack.Screen name="WatchLater" component={WatchLaterScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="WatchLater" component={WatchLaterScreen} options={{headerShown: false}}/>
|
||||||
</Stack.Navigator>
|
<Stack.Screen name="Info" component={InfoScreen} options={{headerShown: false}}/>
|
||||||
);
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A bottom tab navigator displays tab buttons on the bottom of the display to switch screens.
|
* A bottom tab navigator displays tab buttons on the bottom of the display to switch screens.
|
||||||
* https://reactnavigation.org/docs/bottom-tab-navigator
|
* https://reactnavigation.org/docs/bottom-tab-navigator
|
||||||
*/
|
*/
|
||||||
const BottomTab = createBottomTabNavigator<RootTabParamList>();
|
const BottomTab = createBottomTabNavigator<RootTabParamList>()
|
||||||
|
|
||||||
function BottomTabNavigator() {
|
function BottomTabNavigator() {
|
||||||
const colorScheme = useColorScheme();
|
let colorScheme = useColorScheme()
|
||||||
|
const isLightTheme = colorScheme === "light"
|
||||||
return (
|
|
||||||
<BottomTab.Navigator
|
return (
|
||||||
initialRouteName="WatchLater"
|
<BottomTab.Navigator
|
||||||
screenOptions={{
|
initialRouteName="Home"
|
||||||
tabBarActiveTintColor: "purple",
|
screenOptions={{
|
||||||
|
tabBarActiveTintColor: isLightTheme ? "black" : "white",
|
||||||
}}>
|
|
||||||
<BottomTab.Screen
|
}}>
|
||||||
name="WatchLater"
|
<BottomTab.Screen
|
||||||
|
name="WatchLater"
|
||||||
component={WatchLaterScreen}
|
|
||||||
options={({ navigation }: RootTabScreenProps<'WatchLater'>) => ({
|
component={WatchLaterScreen}
|
||||||
tabBarIcon: ({ color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>,
|
options={({navigation}: RootTabScreenProps<'WatchLater'>) => ({
|
||||||
headerShown: false,
|
tabBarIcon: ({color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>,
|
||||||
|
headerShown: false,
|
||||||
})}
|
|
||||||
/>
|
})}
|
||||||
<BottomTab.Screen
|
/>
|
||||||
name="Home"
|
<BottomTab.Screen
|
||||||
component={HomeScreen}
|
name="Home"
|
||||||
|
component={HomeScreen}
|
||||||
options={{
|
|
||||||
headerShown: false,
|
options={{
|
||||||
tabBarIcon: ({ color, size }) => <TabBarIcon name={faFilm} color={color} size={20}/>,
|
headerShown: false,
|
||||||
}}
|
tabBarIcon: ({color, size}) => <TabBarIcon name={faFilm} color={color} size={20}/>,
|
||||||
/>
|
}}
|
||||||
<BottomTab.Screen
|
/>
|
||||||
name="Favorite"
|
<BottomTab.Screen
|
||||||
component={FavoriteScreen}
|
name="Favorite"
|
||||||
|
component={FavoriteScreen}
|
||||||
options={{
|
|
||||||
headerShown: false,
|
options={{
|
||||||
tabBarIcon: ({ color, size }) => <TabBarIcon name={faHeart} color={color} size={20} />,
|
headerShown: false,
|
||||||
}}
|
tabBarIcon: ({color, size}) => <TabBarIcon name={faHeart} color={color} size={20}/>,
|
||||||
/>
|
}}
|
||||||
</BottomTab.Navigator>
|
/>
|
||||||
);
|
</BottomTab.Navigator>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
* You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||||
*/
|
*/
|
||||||
function TabBarIcon(props: {
|
function TabBarIcon(props: {
|
||||||
name: any;
|
name: any;
|
||||||
color: string;
|
color: string;
|
||||||
size: number;
|
size: number;
|
||||||
}) {
|
}) {
|
||||||
return <FontAwesomeIcon icon={props.name} style={{marginBottom: -5}} size={props.size} color={props.color} />;
|
return <FontAwesomeIcon icon={props.name} style={{marginBottom: -5}} size={props.size} color={props.color}/>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,124 @@
|
|||||||
|
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from '../constants'
|
||||||
|
import config from "../../constants/config"
|
||||||
|
import Movie from "../../model/Movie"
|
||||||
|
import {getFavouriteList, getWatchLaterList} from "../../storage/storage"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const setWatchLater = (TrendingMovieList: null | Movie[]) => {
|
||||||
|
return {
|
||||||
|
type: LOAD_WATCHLATER,
|
||||||
|
payload: TrendingMovieList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setFavourite = (FavouriteList: null | Movie[]) => {
|
||||||
|
return {
|
||||||
|
type: LOAD_FAVOURITE,
|
||||||
|
payload: FavouriteList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getWatchLater = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let MovieList = await getWatchLaterList();
|
||||||
|
dispatch(setWatchLater(MovieList));
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getFavourite = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
let MovieList = await getFavouriteList();
|
||||||
|
dispatch(setFavourite(MovieList));
|
||||||
|
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const setinfoMovie = (TrendingMovieList: null | Movie[]) => {
|
||||||
|
return {
|
||||||
|
type: FETCH_TRENDING_MOVIE,
|
||||||
|
payload: TrendingMovieList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTrendingID = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return async dispatch => {
|
||||||
|
try {
|
||||||
|
const IDPromise = await fetch(config.base_url + "trending/movie/day?api_key=" + config.api_key);
|
||||||
|
const IDListJson = await IDPromise.json();
|
||||||
|
// @ts-ignore
|
||||||
|
const idList: string[] = IDListJson.results.map(elt => elt["id"]);
|
||||||
|
const MovieList: Movie[] = [];
|
||||||
|
Promise.all(idList.map(async elt => {
|
||||||
|
try {
|
||||||
|
return await fetch(config.base_url + "movie/" + elt + "?api_key=" + config.api_key + "&language=fr-FR");
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error', err);
|
||||||
|
}
|
||||||
|
})).then(function (responses) {
|
||||||
|
// @ts-ignore
|
||||||
|
Promise.all(responses.map(result => result.json()))
|
||||||
|
.then(function (elements) {
|
||||||
|
elements.forEach(elt => {
|
||||||
|
const infoJson = elt;
|
||||||
|
const genreRow: string[] = [];
|
||||||
|
// @ts-ignore
|
||||||
|
elt["genres"].map(genre => {
|
||||||
|
genreRow.push(genre.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(new Movie(infoJson["id"], infoJson["title"], infoJson["poster_path"], infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"], genreRow, infoJson["overview"], infoJson["backdrop_path"]))
|
||||||
|
// @ts-ignore
|
||||||
|
MovieList.push(new Movie(infoJson["id"], infoJson["title"], infoJson["poster_path"], infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"], genreRow, infoJson["overview"], infoJson["backdrop_path"]))
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
dispatch(setinfoMovie(MovieList));
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error', err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeMovieTrending = (movie: Movie) => {
|
||||||
|
return {
|
||||||
|
type: POP_FIRST_TRENDING,
|
||||||
|
payload: movie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMovieToWatchLater = (movie: Movie) => {
|
||||||
|
return {
|
||||||
|
type: ADD_WATCHLATER,
|
||||||
|
payload: movie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addMovieToFavourite = (movie: Movie) => {
|
||||||
|
return {
|
||||||
|
type: ADD_FAVOURITE,
|
||||||
|
payload: movie
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
export const FETCH_TRENDING_ID : string = "FETCH_TRENDING_ID";
|
||||||
|
export const FETCH_TRENDING_MOVIE: string = "FETCH_TRENDING_MOVIE";
|
||||||
|
export const POP_FIRST_TRENDING: string = "POP_FIRST_TRENDING";
|
||||||
|
|
||||||
|
export const ADD_WATCHLATER : string = "ADD_WATCHLATER";
|
||||||
|
export const FETCH_WATCHLATER : string = "FETCH_WATCHLATER";
|
||||||
|
|
||||||
|
export const ADD_FAVOURITE : string = "ADD_FAVOURITE";
|
||||||
|
export const FETCH_FAVOURITE : string = "FETCH_FAVOURITE";
|
||||||
|
|
||||||
|
export const LOAD_WATCHLATER : string = "LOAD_WATCHLATER";
|
||||||
|
export const LOAD_FAVOURITE : string = "LOAD_FAVOURITE";
|
@ -0,0 +1,32 @@
|
|||||||
|
import {POP_FIRST_TRENDING, FETCH_TRENDING_MOVIE, ADD_WATCHLATER, ADD_FAVOURITE, LOAD_WATCHLATER, LOAD_FAVOURITE} from "../constants";
|
||||||
|
import Movie from "../../model/Movie";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
trendingIDs: [],
|
||||||
|
trendingMovies: [] as Movie[],
|
||||||
|
watchLaterMovies: [] as Movie[],
|
||||||
|
favouriteMovies: [] as Movie[],
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
export default (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case LOAD_WATCHLATER:
|
||||||
|
// @ts-ignore
|
||||||
|
return {...state, watchLaterMovies: action.payload};
|
||||||
|
case LOAD_FAVOURITE:
|
||||||
|
// @ts-ignore
|
||||||
|
return {...state, favouriteMovies: action.payload};
|
||||||
|
case FETCH_TRENDING_MOVIE:
|
||||||
|
return {...state, trendingMovies: action.payload};
|
||||||
|
case POP_FIRST_TRENDING:
|
||||||
|
return {...state, trendingMovies: [...state.trendingMovies.filter((item: Movie) => item !== action.payload)]};
|
||||||
|
case ADD_WATCHLATER:
|
||||||
|
// @ts-ignore
|
||||||
|
return {...state, watchLaterMovies: [action.payload, ...state.watchLaterMovies]};
|
||||||
|
case ADD_FAVOURITE:
|
||||||
|
// @ts-ignore
|
||||||
|
return {...state, favouriteMovies: [action.payload, ...state.favouriteMovies]};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
import appReducer from "./reducers/appReducer";
|
||||||
|
const reducer = {
|
||||||
|
appReducer: appReducer,
|
||||||
|
}
|
||||||
|
|
||||||
|
const store= configureStore({
|
||||||
|
// @ts-ignore
|
||||||
|
reducer: reducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false,
|
||||||
|
}),
|
||||||
|
},);
|
||||||
|
|
||||||
|
export default store;
|
@ -1,89 +1,148 @@
|
|||||||
import {FlatList, StyleSheet, SafeAreaView, Text, View, Image, TextInput} from 'react-native';
|
import {FlatList, StyleSheet, View, TextInput, TouchableHighlight} from 'react-native';
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {BadgeFilm} from "./HomeScreen";
|
import {RootTabScreenProps} from "../types";
|
||||||
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
import { faHeart} from "@fortawesome/free-solid-svg-icons";
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import {RootTabScreenProps} from "../types.js";
|
import {useEffect, useState} from 'react';
|
||||||
|
import {getFavourite} from "../redux/actions/actions";
|
||||||
|
import Movie from "../model/Movie";
|
||||||
|
import {MovieListComponent} from "../components/MovieListComponent";
|
||||||
|
import MovieFinderScreenList from "./MovieFinderScreenList";
|
||||||
|
|
||||||
|
export default function FavoriteScreen({navigation}: RootTabScreenProps<'Favorite'>) {
|
||||||
|
|
||||||
export default function FavoriteScreen({ navigation }: RootTabScreenProps<'Favorite'>) {
|
const [search, setSearch] = useState('');
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container}>
|
|
||||||
<View style={{height: 50, justifyContent: "flex-start",flexDirection: 'row', paddingHorizontal:20, marginBottom: 15,marginVertical:5, alignItems:"flex-end"}} >
|
|
||||||
<FontAwesomeIcon icon={faHeart} style={{marginBottom: -5, marginRight: 20}} size={50} color="white" />
|
|
||||||
<Text style={{color: "white", fontSize:30}}>Favorite</Text>
|
|
||||||
</View>
|
|
||||||
<Image
|
|
||||||
source={require('../assets/images/delimiter.png')} style={{height: 2, width: 400, resizeMode: "stretch"}}
|
|
||||||
/>
|
|
||||||
<View style={{height:40, width:400, backgroundColor:"grey", borderRadius:20, marginVertical:10, alignSelf:"center"}}>
|
|
||||||
<TextInput style={{width:'100%', height:40, marginHorizontal:20}} ></TextInput>
|
|
||||||
</View>
|
|
||||||
<FlatList
|
|
||||||
data={[
|
|
||||||
{key: 'Devin'},
|
|
||||||
{key: 'Dan'},
|
|
||||||
{key: 'Dominic'},
|
|
||||||
{key: 'Jackson'},
|
|
||||||
{key: 'James'},
|
|
||||||
{key: 'Joel'},
|
|
||||||
{key: 'John'},
|
|
||||||
{key: 'Jillian'},
|
|
||||||
{key: 'Jimmy'},
|
|
||||||
{key: 'Julie'},
|
|
||||||
]}
|
|
||||||
renderItem={({item}) => <ListWidget name={item.key} ></ListWidget>}
|
|
||||||
/>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
paddingTop: 22,
|
|
||||||
backgroundColor: "#232323"
|
|
||||||
},
|
|
||||||
item: {
|
|
||||||
padding: 10,
|
|
||||||
fontSize: 18,
|
|
||||||
height: 44,
|
|
||||||
color: "white"
|
|
||||||
},
|
|
||||||
filmCard: {
|
|
||||||
width: 70,
|
|
||||||
height: 100,
|
|
||||||
borderRadius: 8,
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type ListWidgetProps = {
|
|
||||||
name : String
|
|
||||||
|
|
||||||
}
|
const [filteredDataSource, setFilteredDataSource] = useState<Movie[]>([]);
|
||||||
|
|
||||||
export function ListWidget(props: ListWidgetProps) {
|
const [borderwidth, setBorderWidth] = useState(0);
|
||||||
return (
|
|
||||||
<View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'row', paddingHorizontal:20, marginVertical:5}} >
|
|
||||||
<Image
|
|
||||||
style={styles.filmCard}
|
|
||||||
source={{
|
|
||||||
uri: 'https://fr.web.img4.acsta.net/pictures/21/11/16/10/01/4860598.jpg',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'column', paddingLeft:10}} >
|
|
||||||
<Text style={{color: "white", fontWeight:"bold", fontSize:25}}>{props.name}</Text>
|
|
||||||
<Text style={{color: "grey", fontWeight:"bold", fontSize:17}}>{props.name}</Text>
|
|
||||||
<View style={{marginVertical:10}}>
|
|
||||||
<BadgeFilm name={"Science-Ficton"}/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
const [masterDataSource] = useState([]);
|
||||||
|
|
||||||
);
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: insets.top + 22,
|
||||||
|
backgroundColor: "#0E0E0E"
|
||||||
|
},
|
||||||
|
linearGradient: {
|
||||||
|
flex: 1,
|
||||||
|
paddingLeft: 15,
|
||||||
|
paddingRight: 15,
|
||||||
|
borderRadius: 5
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
padding: 10,
|
||||||
|
fontSize: 18,
|
||||||
|
height: 44,
|
||||||
|
color: "white"
|
||||||
|
},
|
||||||
|
filmCard: {
|
||||||
|
width: 70,
|
||||||
|
height: 100,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
searchSection: {
|
||||||
|
height: 40,
|
||||||
|
width: 400,
|
||||||
|
backgroundColor: "#323232",
|
||||||
|
borderRadius: 20,
|
||||||
|
marginVertical: 10,
|
||||||
|
alignSelf: "center",
|
||||||
|
borderWidth: borderwidth,
|
||||||
|
borderColor: "rgba(255,255,255,0.22)"
|
||||||
|
|
||||||
|
},
|
||||||
|
searchBar: {
|
||||||
|
width: '100%',
|
||||||
|
height: 40,
|
||||||
|
marginHorizontal: 20,
|
||||||
|
color: "white"
|
||||||
|
},
|
||||||
|
titlePage: {
|
||||||
|
height: 50,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
marginVertical: 5,
|
||||||
|
alignItems: "flex-end"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginBottom: -5,
|
||||||
|
marginRight: 20
|
||||||
|
},
|
||||||
|
delimiter: {
|
||||||
|
height: 2,
|
||||||
|
width: 400,
|
||||||
|
resizeMode: "stretch"
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
const favouriteMovies = useSelector(state => state.appReducer.favouriteMovies);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadFavourite = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
dispatch(getFavourite());
|
||||||
|
};
|
||||||
|
loadFavourite();
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const searchFilterFunction = (text: string) => {
|
||||||
|
if (text) {
|
||||||
|
const newData = favouriteMovies.filter(function (item: Movie) {
|
||||||
|
const itemData = item.original_title
|
||||||
|
? item.original_title.toUpperCase()
|
||||||
|
: ''.toUpperCase();
|
||||||
|
const textData = text.toUpperCase();
|
||||||
|
return itemData.indexOf(textData) > -1;
|
||||||
|
});
|
||||||
|
setFilteredDataSource(newData);
|
||||||
|
setSearch(text);
|
||||||
|
} else {
|
||||||
|
setFilteredDataSource(masterDataSource);
|
||||||
|
setSearch(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggleSearchBar = () => {
|
||||||
|
if (borderwidth === 0)
|
||||||
|
setBorderWidth(2)
|
||||||
|
else
|
||||||
|
setBorderWidth(0)
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<MovieFinderScreenList page={"Favorite"}>
|
||||||
|
|
||||||
|
<View style={styles.searchSection}>
|
||||||
|
<TextInput style={styles.searchBar} onChangeText={(text) => searchFilterFunction(text)}
|
||||||
|
value={search}
|
||||||
|
placeholder="Rechercher ici..."
|
||||||
|
placeholderTextColor={"white"}
|
||||||
|
onFocus={ToggleSearchBar}
|
||||||
|
onBlur={ToggleSearchBar}
|
||||||
|
></TextInput>
|
||||||
|
</View>
|
||||||
|
<FlatList
|
||||||
|
data={search.length !== 0 ? filteredDataSource : favouriteMovies}
|
||||||
|
keyExtractor={item => item.original_title}
|
||||||
|
// @ts-ignore
|
||||||
|
renderItem={({item}) => <TouchableHighlight onPress={() => navigation.navigate("Info", {"item": item})}><MovieListComponent movie={item}></MovieListComponent></TouchableHighlight>}
|
||||||
|
/>
|
||||||
|
</MovieFinderScreenList>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,477 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {TouchableOpacity, ScrollView, View, Text, StyleSheet, Image, SafeAreaView, FlatList} from 'react-native';
|
||||||
|
import {RootStackScreenProps} from "../types";
|
||||||
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
|
import Movie from "../model/Movie";
|
||||||
|
import {LinearGradient} from 'expo-linear-gradient';
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import config from "../constants/config";
|
||||||
|
import YoutubeIframe from "react-native-youtube-iframe";
|
||||||
|
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||||
|
import MinimalMovie from "../model/MinimalMovie";
|
||||||
|
import Review from "../model/review";
|
||||||
|
import Stars from "../components/StarsComponent";
|
||||||
|
import {formatTime} from "../model/formatTime";
|
||||||
|
|
||||||
|
export default function InfoScreen({navigation, route}: RootStackScreenProps<'Info'>) {
|
||||||
|
// @ts-ignore
|
||||||
|
const item: Movie = route.params.item
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const [trailerPath, setTrailerPath] = useState("");
|
||||||
|
|
||||||
|
const [similarMovies, setSimilarMovies] = useState<MinimalMovie[]>([]);
|
||||||
|
|
||||||
|
const [review, setReview] = useState<Review[]>([]);
|
||||||
|
|
||||||
|
const [credit, setCredit] = useState<creditItem[]>();
|
||||||
|
|
||||||
|
const [paddingTopBackground, setPaddingTopBackground] = useState(0);
|
||||||
|
|
||||||
|
const [opacityBackground, setOpacityBackground] = useState(0.7);
|
||||||
|
|
||||||
|
const [scaleBackground, setScaleBackground] = useState(1);
|
||||||
|
|
||||||
|
|
||||||
|
const handleScroll = (event: any) => {
|
||||||
|
const {y} = event.nativeEvent.contentOffset;
|
||||||
|
let padTop = y / -20;
|
||||||
|
if (padTop <= 0)
|
||||||
|
setPaddingTopBackground(padTop);
|
||||||
|
setOpacityBackground(0.5 - y / 600);
|
||||||
|
let scale = 1 - y / -2000
|
||||||
|
if (scale >= 1)
|
||||||
|
setScaleBackground(scale);
|
||||||
|
};
|
||||||
|
|
||||||
|
type creditItem = [string, string, number];
|
||||||
|
|
||||||
|
type creditProps = {
|
||||||
|
data: creditItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function CreditList({data}: creditProps) {
|
||||||
|
const renderItem = ({item}: { item: creditItem }) => (
|
||||||
|
<View style={styles.creditContainer}>
|
||||||
|
<View style={styles.bubble}>
|
||||||
|
<Image source={{uri: item[1]}} style={styles.photo}></Image>
|
||||||
|
<View style={styles.popularityDot}>
|
||||||
|
<Text style={styles.popularityLabel}>{item[2].toFixed(1).toString()}</Text>
|
||||||
|
<Ionicons name="md-star" size={13} color="#FFC42D"/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text numberOfLines={2} style={styles.creditName}>{item[0]}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
style={{paddingBottom: 40}}
|
||||||
|
data={data}
|
||||||
|
horizontal={true}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item[0]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimilarMovieProps = {
|
||||||
|
movie: MinimalMovie;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SimilarMovie(props: SimilarMovieProps) {
|
||||||
|
return (
|
||||||
|
<View style={styles.similarContainer}>
|
||||||
|
<Image source={{uri: props.movie.poster_path}} style={styles.similarPoster}></Image>
|
||||||
|
<Text numberOfLines={2} style={styles.similarTitleFilm}>{props.movie.original_title}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReviewProps = {
|
||||||
|
review: Review;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ReviewComponent(props: ReviewProps) {
|
||||||
|
return (
|
||||||
|
<View style={styles.reviewContainer}>
|
||||||
|
|
||||||
|
<View style={styles.reviewInfo}>
|
||||||
|
<Image source={{uri: props.review.profile_path}} style={styles.imageProfile}></Image>
|
||||||
|
<View style={styles.infoContainer}>
|
||||||
|
<Text numberOfLines={1} style={styles.pseudo}>{props.review.pseudo}</Text>
|
||||||
|
<Text style={styles.date}>{props.review.date}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
<Text numberOfLines={15} style={styles.message}>{props.review.message}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfoBadgeProps = {
|
||||||
|
texte: string
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoBadge(props: InfoBadgeProps) {
|
||||||
|
return (<View style={{paddingHorizontal: 15, paddingVertical: 7, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 10, justifyContent: "center", marginRight: 10}}>
|
||||||
|
<Text style={{color: "white", fontSize: 15}}>{props.texte}</Text>
|
||||||
|
</View>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
background1: {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
paddingTop: insets.top,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
backgroundColor: "#0E0E0E"
|
||||||
|
},
|
||||||
|
backgroundSection: {
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
position: "absolute"
|
||||||
|
},
|
||||||
|
back_drop: {
|
||||||
|
height: "45%",
|
||||||
|
top: paddingTopBackground,
|
||||||
|
width: '100%',
|
||||||
|
opacity: opacityBackground,
|
||||||
|
position: "absolute",
|
||||||
|
|
||||||
|
transform: [{scale: scaleBackground}],
|
||||||
|
},
|
||||||
|
gradientFade: {
|
||||||
|
height: "30%",
|
||||||
|
top: "25%"
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 10,
|
||||||
|
left: 5
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
height: "100%"
|
||||||
|
},
|
||||||
|
section1: {
|
||||||
|
paddingHorizontal: 35
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 43,
|
||||||
|
fontWeight: "bold",
|
||||||
|
paddingBottom: 10,
|
||||||
|
paddingTop: "45%"
|
||||||
|
},
|
||||||
|
characteristics: {
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start"
|
||||||
|
},
|
||||||
|
stars: {
|
||||||
|
flexDirection: "row",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingBottom: 30
|
||||||
|
},
|
||||||
|
starsLabel: {
|
||||||
|
color: "#FFC42D",
|
||||||
|
fontWeight: "bold",
|
||||||
|
paddingLeft: 10,
|
||||||
|
fontSize: 16
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
borderRadius: 10,
|
||||||
|
overflow: "hidden"
|
||||||
|
},
|
||||||
|
resume: {
|
||||||
|
color: "#B3B3B3",
|
||||||
|
paddingTop: 30,
|
||||||
|
fontSize: 17
|
||||||
|
},
|
||||||
|
creditSection: {
|
||||||
|
paddingTop: 30
|
||||||
|
},
|
||||||
|
creditTitle: {
|
||||||
|
color: "#2998FD",
|
||||||
|
paddingBottom: 20,
|
||||||
|
paddingLeft: 35,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "600"
|
||||||
|
},
|
||||||
|
similarSection: {
|
||||||
|
paddingTop: 30
|
||||||
|
},
|
||||||
|
similarTitle: {
|
||||||
|
color: "#2998FD",
|
||||||
|
paddingLeft: 35,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "600",
|
||||||
|
paddingBottom: 20
|
||||||
|
},
|
||||||
|
similarContainer: {
|
||||||
|
width: 90,
|
||||||
|
marginHorizontal: 7
|
||||||
|
},
|
||||||
|
similarPoster: {
|
||||||
|
height: 130,
|
||||||
|
width: 90,
|
||||||
|
borderRadius: 8
|
||||||
|
},
|
||||||
|
similarTitleFilm: {
|
||||||
|
color: "#DADADA",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "300"
|
||||||
|
},
|
||||||
|
reviewSection: {
|
||||||
|
paddingTop: 30
|
||||||
|
},
|
||||||
|
reviewTitle: {
|
||||||
|
color: "#2998FD",
|
||||||
|
paddingLeft: 35,
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: "600",
|
||||||
|
paddingBottom: 10
|
||||||
|
},
|
||||||
|
reviewContainer: {
|
||||||
|
marginHorizontal: 7,
|
||||||
|
width: 300,
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: "#09090F",
|
||||||
|
marginVertical: 10,
|
||||||
|
borderRadius: 14,
|
||||||
|
borderWidth: 0.8,
|
||||||
|
borderColor: "rgba(223,223,223,0.14)"
|
||||||
|
},
|
||||||
|
reviewInfo: {
|
||||||
|
flexDirection: "row",
|
||||||
|
paddingBottom: 20
|
||||||
|
},
|
||||||
|
imageProfile: {
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
borderRadius: 100
|
||||||
|
},
|
||||||
|
infoContainer: {
|
||||||
|
paddingLeft: 10,
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "80%",
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
pseudo: {
|
||||||
|
color: "white",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 16
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
color: "grey",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "500",
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
color: "#B3B3B3",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "400"
|
||||||
|
},
|
||||||
|
creditContainer: {
|
||||||
|
width: 90,
|
||||||
|
marginHorizontal: 7,
|
||||||
|
alignItems: "center"
|
||||||
|
},
|
||||||
|
bubble: {
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
photo: {
|
||||||
|
height: 90,
|
||||||
|
width: 90,
|
||||||
|
borderRadius: 200,
|
||||||
|
borderWidth: 3,
|
||||||
|
borderColor: "rgba(255,255,255,0.8)"
|
||||||
|
},
|
||||||
|
popularityDot: {
|
||||||
|
backgroundColor: "white",
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 2,
|
||||||
|
paddingHorizontal: 5,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
flexDirection: "row"
|
||||||
|
},
|
||||||
|
popularityLabel: {
|
||||||
|
color: "black",
|
||||||
|
fontWeight: "500",
|
||||||
|
paddingRight: 4
|
||||||
|
},
|
||||||
|
creditName: {
|
||||||
|
color: "#DADADA",
|
||||||
|
paddingTop: 5,
|
||||||
|
fontWeight: "300"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTriller = async () => {
|
||||||
|
const trailerResponse = (await fetch(config.base_url + "movie/" + item.id + "/videos?api_key=" + config.api_key + "&language=fr-FR"));
|
||||||
|
|
||||||
|
const trailerJson = await trailerResponse.json();
|
||||||
|
//console.log("trailer", trailerJson)
|
||||||
|
// @ts-ignore
|
||||||
|
const trailer_key = trailerJson.results.slice(0, 1).map((elt) => {
|
||||||
|
if (elt["type"] === "Trailer" && elt["site"] === "YouTube") {
|
||||||
|
return elt["key"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//console.log("key", trailer_key)
|
||||||
|
setTrailerPath(trailer_key);
|
||||||
|
}
|
||||||
|
const getCredits = async () => {
|
||||||
|
const creditResponse = (await fetch(config.base_url + "movie/" + item.id + "/credits?api_key=" + config.api_key + "&language=fr-FR"));
|
||||||
|
|
||||||
|
const creditJson = await creditResponse.json();
|
||||||
|
// @ts-ignore
|
||||||
|
let creditList = creditJson.cast.map((elt) => {
|
||||||
|
if (elt["popularity"])
|
||||||
|
return [elt["name"], 'https://image.tmdb.org/t/p/w185' + elt["profile_path"], elt["popularity"]]
|
||||||
|
});
|
||||||
|
creditList = creditList.slice(0, 5).sort((a: [fullname: string, profile_path: string, popularity: number], b: [fullname: string, profil_path: string, popularity: number]) => b[2] - a[2]);
|
||||||
|
|
||||||
|
setCredit(creditList);
|
||||||
|
}
|
||||||
|
const getSimilarMovies = async () => {
|
||||||
|
const SimilarMoviesResponse = (await fetch(config.base_url + "movie/" + item.id + "/recommendations?api_key=" + config.api_key + "&language=fr-FR"));
|
||||||
|
|
||||||
|
const SimilarMoviesJson = await SimilarMoviesResponse.json();
|
||||||
|
// @ts-ignore
|
||||||
|
const SimilarMoviesList = SimilarMoviesJson.results.slice(0, 10).map((elt) => {
|
||||||
|
return new MinimalMovie(elt["original_title"], elt["poster_path"])
|
||||||
|
});
|
||||||
|
setSimilarMovies(SimilarMoviesList);
|
||||||
|
}
|
||||||
|
const getReview = async () => {
|
||||||
|
const ReviewResponse = (await fetch(config.base_url + "movie/" + item.id + "/reviews?api_key=" + config.api_key + "&language=us-EN&page=1"));
|
||||||
|
const ReviewJson = await ReviewResponse.json();
|
||||||
|
// @ts-ignore
|
||||||
|
let ReviewList = ReviewJson.results.slice(0, 5).map((elt) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const newreview = new Review(elt["content"], elt["author_details"].avatar_path, elt["created_at"], elt["author"])
|
||||||
|
return newreview
|
||||||
|
});
|
||||||
|
ReviewList = ReviewList.filter((review: Review, index: number, array: Review[]) => {
|
||||||
|
return array.findIndex((item: Review) => item.pseudo === review.pseudo) === index;
|
||||||
|
});
|
||||||
|
setReview(ReviewList);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
|
||||||
|
getTriller();
|
||||||
|
getSimilarMovies();
|
||||||
|
getCredits();
|
||||||
|
getReview();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.body}>
|
||||||
|
<View style={styles.backgroundSection}>
|
||||||
|
<Image
|
||||||
|
style={styles.back_drop}
|
||||||
|
source={{
|
||||||
|
uri: item.backdrop_path,
|
||||||
|
}}
|
||||||
|
></Image>
|
||||||
|
|
||||||
|
<LinearGradient
|
||||||
|
// Background Linear Gradient
|
||||||
|
colors={['rgba(0,0,0,0.8)', 'transparent']}
|
||||||
|
/>
|
||||||
|
<LinearGradient style={styles.gradientFade}
|
||||||
|
// Button Linear Gradient
|
||||||
|
colors={['rgba(14,14,14,0)', 'rgba(14,14,14,0.7)', 'rgba(14,14,14,1)', 'rgba(14,14,14,1)']}>
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
<SafeAreaView style={styles.background1}>
|
||||||
|
<TouchableOpacity onPress={() => navigation.goBack()} style={{zIndex: 100}}>
|
||||||
|
<Ionicons name="ios-arrow-back" size={30} color="white" style={styles.backButton}/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<ScrollView style={styles.list} showsVerticalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={4}
|
||||||
|
>
|
||||||
|
<View style={styles.section1}>
|
||||||
|
<Text style={styles.title} numberOfLines={2}>{item.original_title}</Text>
|
||||||
|
<View style={styles.characteristics}>
|
||||||
|
<InfoBadge texte={`${item.genres[0]} ${item.genres[1] !== undefined ? ", " + item.genres[1] : ""}`}></InfoBadge>
|
||||||
|
<InfoBadge texte={item.release_date}></InfoBadge>
|
||||||
|
<InfoBadge texte={formatTime(item.runtime)}></InfoBadge>
|
||||||
|
</View>
|
||||||
|
<View style={styles.stars}>
|
||||||
|
<Stars note={item.vote_average} size={120}></Stars>
|
||||||
|
<Text style={styles.starsLabel}>{item.vote_average.toFixed(1)}</Text>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
{trailerPath !== "" && (<YoutubeIframe webViewStyle={styles.player} height={195} play={false} videoId={trailerPath}/>)}
|
||||||
|
<Text style={styles.resume}>{item.overview}</Text>
|
||||||
|
|
||||||
|
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{credit !== undefined && (
|
||||||
|
<View style={styles.creditSection}>
|
||||||
|
<Text style={styles.creditTitle}>Crédits</Text>
|
||||||
|
<CreditList data={credit}></CreditList>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
{similarMovies.length !== 0 && (
|
||||||
|
<View style={styles.similarSection}>
|
||||||
|
<Text style={styles.similarTitle}>Recommendations</Text>
|
||||||
|
<FlatList
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
data={similarMovies}
|
||||||
|
horizontal={true}
|
||||||
|
keyExtractor={item => item.original_title}
|
||||||
|
renderItem={({item}) =>
|
||||||
|
<SimilarMovie movie={item}></SimilarMovie>
|
||||||
|
}
|
||||||
|
/></View>
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
{review.length !== 0 && (
|
||||||
|
<View style={styles.reviewSection}>
|
||||||
|
<Text style={styles.reviewTitle}>Commentaires</Text>
|
||||||
|
<FlatList
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
data={review}
|
||||||
|
horizontal={true}
|
||||||
|
keyExtractor={item => item.pseudo}
|
||||||
|
renderItem={({item}) =>
|
||||||
|
<ReviewComponent review={item}></ReviewComponent>
|
||||||
|
}
|
||||||
|
/></View>
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import {Image, SafeAreaView, StyleSheet, Text, View} from "react-native";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
|
||||||
|
import {faClock, faHeart} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import * as React from "react";
|
||||||
|
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: string;
|
||||||
|
children : React.PropsWithChildren<{}>
|
||||||
|
};
|
||||||
|
export default function MovieFinderScreenList(props : Props){
|
||||||
|
|
||||||
|
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
paddingTop: insets.top + 22,
|
||||||
|
backgroundColor: "#0E0E0E"
|
||||||
|
},
|
||||||
|
titlePage: {
|
||||||
|
height: 50,
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
marginVertical: 5,
|
||||||
|
alignItems: "flex-end"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginBottom: -5,
|
||||||
|
marginRight: 20
|
||||||
|
},
|
||||||
|
delimiter: {
|
||||||
|
height: 2,
|
||||||
|
width: 400,
|
||||||
|
resizeMode: "stretch"
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
color: "white",
|
||||||
|
fontSize: 30
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<SafeAreaView style={styles.container}>
|
||||||
|
<View style={styles.titlePage}>
|
||||||
|
<FontAwesomeIcon icon={props.page=="Favorite"?faHeart:faClock} style={styles.icon} size={40} color="white"/>
|
||||||
|
{props.page=="Favorite"?<Text style={styles.h1}>Favourite</Text>:<Text style={styles.h1}>Watch Later</Text>}
|
||||||
|
</View>
|
||||||
|
<Image source={require('../assets/images/delimiter.png')} style={styles.delimiter}/>
|
||||||
|
{props.children}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,68 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
import Movie from "../model/Movie"
|
||||||
|
|
||||||
|
export const getFavouriteList = async () => {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem('favourite');
|
||||||
|
if (value === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const favouriteList: Movie[] = await JSON.parse(value)
|
||||||
|
return favouriteList
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setFavouriteList = async (favouriteList: Movie[]) => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('favourite', JSON.stringify(favouriteList))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWatchLaterList = async () => {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem('watchLater')
|
||||||
|
if (value === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const watchLaterList: Movie[] = await JSON.parse(value)
|
||||||
|
return watchLaterList
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setWatchLaterList = async (watchLaterList: Movie[]) => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('watchLater', JSON.stringify(watchLaterList))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMovieList = async () => {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem('movie')
|
||||||
|
if (value === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const movieList: Movie[] = await JSON.parse(value)
|
||||||
|
return movieList
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setMovieList = async (movieList: Movie[]) => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('movie', JSON.stringify(movieList))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|