Compare commits

..

No commits in common. 'master' and 'redux_test_david' have entirely different histories.

@ -1,19 +0,0 @@
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,23 +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 store from "./redux/store" import {View} from "react-native";
import {Provider} from "react-redux" 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 (
<Provider store={store}> <Provider store={store}>
<SafeAreaProvider> <SafeAreaProvider>
<Navigation colorScheme={colorScheme}/> <Navigation colorScheme={colorScheme} />
<StatusBar/> <StatusBar />
</SafeAreaProvider> </SafeAreaProvider>
</Provider> </Provider>
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

@ -1,89 +0,0 @@
<p align="center">
<img src="https://codefirst.iut.uca.fr/git/lucas.delanier/MovieFinder/raw/branch/master/Documentation/banner_image.png " />
</p>
![React Native](https://img.shields.io/badge/react_native-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB)
![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
![C#](https://img.shields.io/badge/c%23-%23239120.svg?style=for-the-badge&logo=c-sharp&logoColor=white)
![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge&logo=redux&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-2496ED.svg?style=for-the-badge&logo=Docker&logoColor=white)
![Jest](https://img.shields.io/badge/Jest-C21325.svg?style=for-the-badge&logo=Jest&logoColor=white)</br>
[![Maintainability Rating](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=sqale_rating&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Reliability Rating](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=reliability_rating&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Security Rating](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=security_rating&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Bugs](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=bugs&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Code Smells](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=code_smells&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Technical Debt](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=sqale_index&token=59656240a4130edba83931f3226a84d76ad9028f)](https://codefirst.iut.uca.fr/sonar/dashboard?id=MovieFinder)
[![Vulnerabilities](https://codefirst.iut.uca.fr/sonar/api/project_badges/measure?project=MovieFinder&metric=vulnerabilities&token=59656240a4130edba83931f3226a84d76ad9028f)](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
![](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png)
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
```
![](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png)
## :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**
![](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png)
## ✨ 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>

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,9 +1,6 @@
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',
]
}; };
}; };

@ -1,37 +0,0 @@
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>);
}

@ -1,98 +0,0 @@
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>
);
}

@ -1,46 +0,0 @@
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;

@ -1,44 +0,0 @@
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>
);
}

@ -0,0 +1,10 @@
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();
});

@ -1,216 +0,0 @@
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);
});
})

@ -1,172 +0,0 @@
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: [],
})
})
})

@ -1,32 +0,0 @@
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"
]
}
*/

@ -1,27 +0,0 @@
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>
);
}

@ -1,3 +0,0 @@
jest.mock('@react-native-async-storage/async-storage', () =>
require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

@ -1,16 +0,0 @@
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;

@ -1,33 +0,0 @@
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,22 @@
class Movie {
public original_title: string
public poster_path: string
public runtime: number
public vote_average : number
public release_date: string
constructor(original_title: string, poster_path: string,runtime: number, vote_average: number, release_date : string) {
this.original_title = original_title;
this.poster_path = 'https://image.tmdb.org/t/p/w500'+poster_path;
this.runtime = runtime;
this.vote_average = vote_average;
this.release_date = release_date;
}
}
export default Movie;

@ -1,6 +0,0 @@
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`;
}

@ -1,28 +0,0 @@
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;

@ -4,10 +4,10 @@
* https://reactnavigation.org/docs/configuring-links * https://reactnavigation.org/docs/configuring-links
*/ */
import {LinkingOptions} from '@react-navigation/native'; import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking'; import * as Linking from 'expo-linking';
import {RootStackParamList} from '../types'; import { RootStackParamList } from '../types';
const linking: LinkingOptions<RootStackParamList> = { const linking: LinkingOptions<RootStackParamList> = {
prefixes: [Linking.createURL('/')], prefixes: [Linking.createURL('/')],
@ -30,12 +30,6 @@ const linking: LinkingOptions<RootStackParamList> = {
FavoriteScreen: 'Favorite', FavoriteScreen: 'Favorite',
}, },
}, },
Info: {
screens: {
FavoriteScreen: 'InfoScreen',
},
},
}, },
}, },
Modal: 'modal', Modal: 'modal',

@ -1,67 +1,33 @@
import {FontAwesomeIcon} from "@fortawesome/react-native-fontawesome" /**
import {faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons" * If you are not familiar with React Navigation, refer to the "Fundamentals" guide:
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs' * https://reactnavigation.org/docs/getting-started
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native' *
import {createNativeStackNavigator} from '@react-navigation/native-stack' */
import * as React from 'react' import { FontAwesome } from '@expo/vector-icons';
import {ColorSchemeName} from 'react-native' import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
import useColorScheme from '../hooks/useColorScheme' import { faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons";
import NotFoundScreen from '../screens/NotFoundScreen' import Ionicons from '@expo/vector-icons/Ionicons';
import WatchLaterScreen from '../screens/WatchLaterScreen' import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import FavoriteScreen from '../screens/FavoriteScreen' import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
import HomeScreen from '../screens/HomeScreen' import { createNativeStackNavigator } from '@react-navigation/native-stack';
import InfoScreen from '../screens/InfoScreen' import * as React from 'react';
import {RootStackParamList, RootTabParamList, RootTabScreenProps} from '../types' import { ColorSchemeName, Pressable } from 'react-native';
import LinkingConfiguration from './LinkingConfiguration'
import {useCallback, useEffect, useState} from "react" import Colors from '../constants/Colors';
import {useDispatch} from "react-redux" import useColorScheme from '../hooks/useColorScheme';
import {getTrendingID, loadWatchLater} from "../redux/actions/actions" import NotFoundScreen from '../screens/NotFoundScreen';
import * as SplashScreen from 'expo-splash-screen' import WatchLaterScreen from '../screens/WatchLaterScreen';
import FavoriteScreen from '../screens/FavoriteScreen';
export default function Navigation({colorScheme}: { colorScheme: ColorSchemeName }) { import HomeScreen from '../screens/HomeScreen';
const [appIsReady, setAppIsReady] = useState(false) import { RootStackParamList, RootTabParamList, RootTabScreenProps } from '../types';
const dispatch = useDispatch() import LinkingConfiguration from './LinkingConfiguration';
useEffect(() => {
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
async function prepare() {
try {
const loadTrendingID = async () => {
// @ts-ignore
dispatch(getTrendingID())
};
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 ( return (
<NavigationContainer <NavigationContainer
linking={LinkingConfiguration} linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<RootNavigator/> <RootNavigator />
</NavigationContainer> </NavigationContainer>
); );
} }
@ -70,17 +36,16 @@ export default function Navigation({colorScheme}: { colorScheme: ColorSchemeName
* 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.Screen name="Info" component={InfoScreen} options={{headerShown: false}}/>
</Stack.Navigator> </Stack.Navigator>
); );
} }
@ -89,25 +54,24 @@ function RootNavigator() {
* 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() {
let colorScheme = useColorScheme() const colorScheme = useColorScheme();
const isLightTheme = colorScheme === "light"
return ( return (
<BottomTab.Navigator <BottomTab.Navigator
initialRouteName="Home" initialRouteName="Home"
screenOptions={{ screenOptions={{
tabBarActiveTintColor: isLightTheme ? "black" : "white", tabBarActiveTintColor: "purple",
}}> }}>
<BottomTab.Screen <BottomTab.Screen
name="WatchLater" name="WatchLater"
component={WatchLaterScreen} component={WatchLaterScreen}
options={({navigation}: RootTabScreenProps<'WatchLater'>) => ({ options={({ navigation }: RootTabScreenProps<'WatchLater'>) => ({
tabBarIcon: ({color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>, tabBarIcon: ({ color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>,
headerShown: false, headerShown: false,
})} })}
@ -118,7 +82,7 @@ function BottomTabNavigator() {
options={{ options={{
headerShown: false, headerShown: false,
tabBarIcon: ({color, size}) => <TabBarIcon name={faFilm} color={color} size={20}/>, tabBarIcon: ({ color, size }) => <TabBarIcon name={faFilm} color={color} size={20}/>,
}} }}
/> />
<BottomTab.Screen <BottomTab.Screen
@ -127,7 +91,7 @@ function BottomTabNavigator() {
options={{ options={{
headerShown: false, headerShown: false,
tabBarIcon: ({color, size}) => <TabBarIcon name={faHeart} color={color} size={20}/>, tabBarIcon: ({ color, size }) => <TabBarIcon name={faHeart} color={color} size={20} />,
}} }}
/> />
</BottomTab.Navigator> </BottomTab.Navigator>
@ -142,5 +106,5 @@ function TabBarIcon(props: {
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} />;
} }

33749
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -7,12 +7,10 @@
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"test": "jest --coverage --coverageDirectory=./components/__tests__" "test": "jest --watchAll"
}, },
"jest": { "jest": {
"preset": "jest-expo", "preset": "jest-expo"
"setupFiles": ["./jestSetupFile.js"]
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^13.0.0", "@expo/vector-icons": "^13.0.0",
@ -21,15 +19,10 @@
"@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-native-fontawesome": "^0.3.0", "@fortawesome/react-native-fontawesome": "^0.3.0",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-navigation/bottom-tabs": "^6.0.5", "@react-navigation/bottom-tabs": "^6.0.5",
"@react-navigation/native": "^6.0.2", "@react-navigation/native": "^6.0.2",
"@react-navigation/native-stack": "^6.1.0", "@react-navigation/native-stack": "^6.1.0",
"@reacticons/ionicons": "^6.0.4", "@reacticons/ionicons": "^6.0.4",
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-native": "^5.4.2",
"@testing-library/react-native": "^12.0.1",
"deprecated-react-native-prop-types": "^4.0.0",
"expo": "~47.0.12", "expo": "~47.0.12",
"expo-asset": "~8.7.0", "expo-asset": "~8.7.0",
"expo-constants": "~14.0.2", "expo-constants": "~14.0.2",
@ -40,40 +33,25 @@
"expo-status-bar": "~1.4.2", "expo-status-bar": "~1.4.2",
"expo-system-ui": "~2.0.1", "expo-system-ui": "~2.0.1",
"expo-web-browser": "~12.0.0", "expo-web-browser": "~12.0.0",
"lottie-react-native": "^5.1.5",
"moment": "^2.29.4",
"react": "18.1.0", "react": "18.1.0",
"react-dom": "18.1.0", "react-dom": "18.1.0",
"react-native": "0.70.5", "react-native": "0.70.5",
"react-native-cards-swipe": "^1.2.1",
"react-native-gesture-handler": "^2.9.0",
"react-native-ionicons": "^4.6.5", "react-native-ionicons": "^4.6.5",
"react-native-linear-gradient": "^2.6.2", "react-native-linear-gradient": "^2.6.2",
"react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1", "react-native-safe-area-context": "4.4.1",
"react-native-screens": "~3.18.0", "react-native-screens": "~3.18.0",
"react-native-svg": "13.4.0", "react-native-svg": "^13.7.0",
"react-native-vector-icons": "^9.2.0",
"react-native-video": "^2.3.1",
"react-native-video-player": "^0.14.0",
"react-native-web": "~0.18.9", "react-native-web": "~0.18.9",
"react-native-webview": "11.23.1",
"react-native-youtube-iframe": "^2.2.2",
"react-redux": "^8.0.5",
"redux": "^4.2.1",
"rive-react-native": "^3.0.41" "rive-react-native": "^3.0.41"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.9", "@babel/core": "^7.12.9",
"@jest/globals": "^29.5.0",
"@types/jest": "^29.5.0",
"@types/react": "~18.0.24", "@types/react": "~18.0.24",
"@types/react-native": "~0.70.6", "@types/react-native": "~0.70.6",
"jest": "^29.5.0", "jest": "^26.6.3",
"jest-expo": "~47.0.1", "jest-expo": "~47.0.1",
"react-test-renderer": "18.1.0", "react-test-renderer": "18.1.0",
"ts-jest": "^29.0.5", "typescript": "^4.6.3"
"typescript": "^4.9.5"
}, },
"private": true "private": true
} }

Binary file not shown.

@ -0,0 +1,85 @@
import {
FETCH_TRENDING_MOVIE,
FETCH_TRENDING_ID,
POP_FIRST_TRENDING,
ADD_WATCHLATER,
FETCH_WATCHLATER,
} from '../constants';
import config from "../../constants/config";
import Movie from "../../model/Movie";
export const setTrendingID = (TrendingIDList: Movie[]) => {
return {
type: FETCH_TRENDING_ID,
payload: TrendingIDList,
};
}
export const fetchWatchLater = (WatchLaterList: Movie[]) => {
return {
type: FETCH_WATCHLATER,
payload: WatchLaterList,
};
}
export const setinfoMovie = (TrendingMovieList: 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{
const infoPromise = await fetch(config.base_url + "movie/"+elt+"?api_key=" + config.api_key);
//const infoJson = await infoPromise.json();
//console.log('infos---------', infoJson);
//MovieList.push(new Movie(infoJson["original_title"], infoJson["poster_path"],infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"]))
return infoPromise;
}catch (err){
console.log('ErrorGet---------', err);
}
})).then(function (responses){
Promise.all(responses.map(result=>result.json()))
.then(function (elements){
elements.map(elt=> {
const infoJson = elt;
console.log('infos---------', elt);
MovieList.push(new Movie(infoJson["original_title"], infoJson["poster_path"],infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"]))
})
console.log("tortue", MovieList)
dispatch(setinfoMovie(MovieList));
})
});
} catch (error) {
console.log('Error---------', error);
}
}
}
export const removeMovieTrending = (movie: Movie) => {
return{
type: POP_FIRST_TRENDING,
payload: movie
}
}
export const addMovieToWatchLater = (movie : any) => {
return{
type: ADD_WATCHLATER,
payload: movie
}
}

@ -1,124 +0,0 @@
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
}
}

@ -1,12 +0,0 @@
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,8 @@
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";

@ -1,32 +0,0 @@
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,29 @@
import {POP_FIRST_TRENDING, FETCH_TRENDING_MOVIE, FETCH_TRENDING_ID, ADD_WATCHLATER, FETCH_WATCHLATER} from "../constants";
import Movie from "../../model/Movie";
const initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
}
// @ts-ignore
export default appReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_TRENDING_ID:
// @ts-ignore
return {...state, trendingIDs: action.payload};
case FETCH_WATCHLATER:
// @ts-ignore
return {...state, watchLaterMovies: 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,trendingMovies:action.payload};
default:
return state;
}
}

@ -1,16 +1,14 @@
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import appReducer from "./reducers/appReducer"; import appReducer from "./reducers/appReducer";
const reducer = { const reducer = {
appReducer: appReducer, appReducer: appReducer,
} }
const store= configureStore({ const store= configureStore({
// @ts-ignore // @ts-ignore
reducer: reducer, reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
},); },);
export default store; export default store;

@ -1,37 +1,47 @@
import {FlatList, StyleSheet, View, TextInput, TouchableHighlight} from 'react-native'; import {FlatList, StyleSheet, SafeAreaView, Text, View, Image, TextInput} from 'react-native';
import * as React from "react"; import * as React from "react";
import {BadgeFilm} from "./HomeScreen";
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
import { faHeart} from "@fortawesome/free-solid-svg-icons";
import {RootTabScreenProps} from "../types"; import {RootTabScreenProps} from "../types";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useDispatch, useSelector} from 'react-redux';
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'>) {
const [search, setSearch] = useState(''); export default function FavoriteScreen({ navigation }: RootTabScreenProps<'Favorite'>) {
return (
const [filteredDataSource, setFilteredDataSource] = useState<Movie[]>([]); <SafeAreaView style={styles.container}>
<View style={{height: 50, justifyContent: "flex-start",flexDirection: 'row', paddingHorizontal:20, marginBottom: 15,marginVertical:5, alignItems:"flex-end"}} >
const [borderwidth, setBorderWidth] = useState(0); <FontAwesomeIcon icon={faHeart} style={{marginBottom: -5, marginRight: 20}} size={50} color="white" />
<Text style={{color: "white", fontSize:30}}>Favorite</Text>
const [masterDataSource] = useState([]); </View>
<Image
const insets = useSafeAreaInsets(); source={require('../assets/images/delimiter.png')} style={{height: 2, width: 400, resizeMode: "stretch"}}
/>
const styles = StyleSheet.create({ <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: { container: {
flex: 1, flex: 1,
paddingTop: insets.top + 22, paddingTop: 22,
backgroundColor: "#0E0E0E" backgroundColor: "#232323"
},
linearGradient: {
flex: 1,
paddingLeft: 15,
paddingRight: 15,
borderRadius: 5
}, },
item: { item: {
padding: 10, padding: 10,
@ -43,106 +53,37 @@ export default function FavoriteScreen({navigation}: RootTabScreenProps<'Favorit
width: 70, width: 70,
height: 100, height: 100,
borderRadius: 8, 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(() => { type ListWidgetProps = {
const loadFavourite = async () => { name : String
// @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 = () => { export function ListWidget(props: ListWidgetProps) {
if (borderwidth === 0) return (
setBorderWidth(2) <View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'row', paddingHorizontal:20, marginVertical:5}} >
else <Image
setBorderWidth(0) 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>
};
);
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>
);
} }

@ -1,61 +1,54 @@
import * as React from 'react'; import * as React from 'react';
import {TouchableOpacity, View, Text, StyleSheet, Image, ImageBackground, SafeAreaView} from 'react-native'; import {
import {useEffect, useState} from "react"; Button,
TouchableOpacity,
ScrollView,
View,
Text,
StyleSheet,
Image,
ImageBackground,
SafeAreaView,
ActivityIndicator
} from 'react-native';
import {RootStackScreenProps} from "../types";
import Rive from 'rive-react-native';
import {useEffect, useRef, useState} from "react";
import {RiveViewManager} from "rive-react-native/lib/typescript/Rive.js";
import {useSafeAreaInsets} from "react-native-safe-area-context"; import {useSafeAreaInsets} from "react-native-safe-area-context";
import {addMovieToWatchLater, addMovieToFavourite, removeMovieTrending,} from "../redux/actions/actions"; import {addMovieToWatchLater, getTrendingID, removeMovieTrending,} from "../redux/actions/actionGetTrendingID";
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import Movie from "../model/Movie"; import Movie from "../model/Movie";
import moment from 'moment';
import CardsSwipe from 'react-native-cards-swipe';
import AnimatedLottieView from "lottie-react-native";
import {Timer, Timer2} from "../components/TimerComponent";
import {HeaderMovie} from "../components/HeaderMovieComponent";
import {NewCard, SuggestedCard} from "../components/cards";
import {setFavouriteList,setWatchLaterList} from "../storage/storage"
export default function HomeScreen({ navigation }: RootStackScreenProps<'Home'>) {
export default function HomeScreen() {
// @ts-ignore
const trendingMovies = useSelector(state => state.appReducer.trendingMovies)
// @ts-ignore // @ts-ignore
const watchLaterMovies = useSelector(state => state.appReducer.watchLaterMovies) const trendingMovies = useSelector(state => state.appReducer.trendingMovies);
// @ts-ignore
const favouriteMovies = useSelector(state => state.appReducer.favouriteMovies)
const dispatch = useDispatch()
const [hours, setHours] = useState(0)
const [minutes, setMinutes] = useState(0)
const [seconds, setSeconds] = useState(0)
const [displayIndex, setdisplayIndex] = useState(0);
const [suggestedMovies, setSuggestedMovies] = useState<number[]>([])
let swiper: any = null console.log("liste [0]: ", trendingMovies[0]);
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets();
const styles = StyleSheet.create({ const styles = StyleSheet.create({
background1: { background: {
backgroundColor: 'black', backgroundColor: 'black',
height: '100%', height: '100%',
width: '100%',
paddingTop: insets.top, paddingTop: insets.top,
}, },
background2: {
height: '100%', container:{
width: '100%',
paddingTop: insets.top,
},
container: {
flex: 1, flex: 1,
}, },
filmCard: { filmCard: {
width: '85%', width: '80%',
justifyContent: 'center', height: '60%',
marginLeft: 'auto', justifyContent:'center',
marginRight: 'auto', marginLeft:'auto',
borderRadius: 22, marginRight:'auto',
flex: 0.80, borderRadius: 15,
alignItems: 'center',
},
image: {
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center',
shadowColor: "#000", shadowColor: "#000",
shadowOffset: { shadowOffset: {
width: 0, width: 0,
@ -63,11 +56,10 @@ export default function HomeScreen() {
}, },
shadowOpacity: 0.39, shadowOpacity: 0.39,
shadowRadius: 8.30, shadowRadius: 8.30,
flex: 1,
paddingTop: 70,
alignSelf: 'center', alignSelf: 'center',
elevation: 13, elevation: 13,
zIndex: 15
}, },
backgroundImage: { backgroundImage: {
flex: 1, flex: 1,
@ -77,276 +69,156 @@ export default function HomeScreen() {
flex: 1, flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)', backgroundColor: 'rgba(0,0,0,0.5)',
}, },
explanation: {
color: "grey",
fontWeight: "400",
paddingHorizontal: 70,
textAlign: "center"
},
h1: {
color: "white",
fontWeight: "600",
fontSize: 35
},
congratsSection: {
alignItems: "center",
width: "100%",
height: "100%",
justifyContent: "center",
zIndex: 1
},
button: {
resizeMode: "stretch",
height: '55%',
aspectRatio: 1,
},
buttonSection: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: "space-evenly",
paddingHorizontal: 30,
width: '100%',
position: "absolute",
top: "74%",
zIndex: 30
},
posterBackground: {
width: "150%",
height: "150%",
justifyContent: "center",
alignItems: "center",
opacity: 0.55,
position: 'absolute',
left: "-50%",
top: "-50%"
},
finishBackground: {
width: "110%",
height: "110%",
justifyContent: "center",
alignItems: "center",
opacity: 0.15,
position: "absolute",
zIndex: 0
}
});
})
const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
setInterval(() => { const loadTrendingID = async () => {
const today = moment()
today.set({hour: 0, minute: 0, second: 0, millisecond: 0})
const tonight = today.add(1, 'days')
const timestamp = tonight.valueOf()
const now = new Date()
const difference = timestamp - now.getTime()
const h = Math.floor(
(difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)
)
setHours(h)
const m = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60))
setMinutes(m)
const s = Math.floor((difference % (1000 * 60)) / 1000)
setSeconds(s)
});
getSuggested()
}, []);
const getSuggested = async () => {
const suggestedResponse = (await fetch("https://codefirst.iut.uca.fr/containers/lucasdelanier-containermoviefinder/api/Suggested"))
const suggestedJson = await suggestedResponse.json()
//console.log("trailer", trailerJson)
// @ts-ignore // @ts-ignore
const suggestedMovies = suggestedJson.map((element) => { await dispatch(getTrendingID());
return element };
console.log("test1:", trendingMovies);
}) loadTrendingID();
console.log("suggested", suggestedMovies) }, [dispatch]);
setSuggestedMovies(suggestedMovies)
}
function addWatchLater(props: Movie) {
const newWatchLaterMovies = [props, ...watchLaterMovies]
if(watchLaterMovies.filter((movie : Movie) => movie.original_title === props.original_title).length > 0){
return null
}
else{
dispatch(addMovieToWatchLater(props))
dispatch(removeMovieTrending(props))
setWatchLaterList(newWatchLaterMovies)
if (displayIndex == trendingMovies.length - 1) {
setdisplayIndex(0)
swiper.swipeLeft()
}
}
} type ItemProps = {
movie : Movie
function addFavourite(props: Movie) {
const newFavouriteMovies = [props, ...favouriteMovies]
if(favouriteMovies.filter((movie : Movie) => movie.original_title === props.original_title).length > 0){
return null
}
else{
dispatch(addMovieToFavourite(props))
dispatch(removeMovieTrending(props))
setFavouriteList(newFavouriteMovies)
if (displayIndex == trendingMovies.length - 1) {
setdisplayIndex(0)
swiper.swipeLeft()
}
}
} }
function addWatchLater(props: Movie){
//dispatch(addMovieToWatchLater(props));
function popFirstTrending(props: Movie) { dispatch(removeMovieTrending(props));
dispatch(removeMovieTrending(props))
if (displayIndex == trendingMovies.length - 1) {
setdisplayIndex(0)
swiper.swipeLeft()
}
} }
return(
return (
<> <>
<ImageBackground blurRadius={0}
style={styles.finishBackground}
source={require("../assets/images/background.png")
}
></ImageBackground>
{trendingMovies.length !== 0 && ( {trendingMovies.length !== 0 && (
<SafeAreaView style={styles.background}>
<SafeAreaView style={styles.background1}> <ImageBackground blurRadius={20}
style={{
position: 'absolute',
<ImageBackground blurRadius={29} width: "120%",
style={styles.posterBackground} height: "120%",
justifyContent: "center",
alignItems: "center",
opacity: 0.28
}}
source={{ source={{
uri: trendingMovies[displayIndex]?.poster_path, uri: trendingMovies[0].poster_path,
}} }}
></ImageBackground> ></ImageBackground>
<HeaderMovie movie={trendingMovies[displayIndex]}></HeaderMovie> <View style={styles.image}>
<CardsSwipe
ref={(rf) => {
swiper = rf
}}
containerStyle={{zIndex: 20}}
cards={trendingMovies}
loop={true}
onSwipedLeft={(index) => {
if (index < trendingMovies.length - 1) {
setdisplayIndex(index + 1);
} else
setdisplayIndex(0)
}
}
onSwipedRight={(index) => {
if (index < trendingMovies.length)
setdisplayIndex(index + 1)
else
setdisplayIndex(0)
}}
renderCard={(card) =>
(
card != undefined && (
<>
<View style={{position: "absolute", zIndex: 20, top: 80, right: 20, alignItems: "flex-end"}}>
{suggestedMovies.includes(card.id) && (<SuggestedCard></SuggestedCard>)}
{(new Date().setDate(new Date().getDate() - 14) < new Date(card.full_date).getTime()) && (<NewCard></NewCard>)}
</View>
<Image <Image
style={styles.filmCard} style={styles.filmCard}
source={{ source={{
uri: card?.poster_path, uri: trendingMovies[0].poster_path,
}} }}
/> />
</View>
<View style={{height:35, marginTop: 10, marginBottom: 15}}>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}>
<BadgeGenre name={"Popular"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"Trending"} isSelected={true}></BadgeGenre>
<BadgeGenre name={"Classic"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"New"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"Cartoon"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"Serie"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre>
</ScrollView>
</View>
<View style={{ flexDirection: 'column', alignSelf: 'flex-start', alignItems: 'flex-start', paddingHorizontal: 30, flex: 1 }}>
</> <View style={{ flexDirection: 'row', alignSelf: 'flex-start', justifyContent: 'flex-start', width: "100%"}}>
) <BadgeFilm name={"Science-fiction"}></BadgeFilm>
) <BadgeFilm name={"Science-fiction"}></BadgeFilm>
} <BadgeFilm name={"9:11"}></BadgeFilm>
/> </View>
<View>
<Text numberOfLines={1} style={{color: "white", fontSize: 28, fontWeight: "bold", paddingTop: 5}}>{trendingMovies[0].original_title}</Text>
<View style={styles.buttonSection}> </View>
<TouchableOpacity onPress={() => { <Text style={{color: "grey", fontSize: 20, fontWeight: "bold"}}>{trendingMovies[0].release_date}</Text>
</View>
addWatchLater(trendingMovies[displayIndex]); <View style={{ flexDirection: 'row' ,alignItems: 'center', justifyContent: "space-evenly", paddingHorizontal: 30, height: '15%', width:'100%'}}>
<TouchableOpacity onPress={() => addWatchLater(trendingMovies[0])}>
}}>
<Image <Image
source={require('../assets/images/watchlater_button.png')} style={styles.button} source={require('../assets/images/WatchLater.png')} style={{ resizeMode:"stretch", height:'65%', aspectRatio: 1,}}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={ <TouchableOpacity>
() => {
popFirstTrending(trendingMovies[displayIndex]);
}}>
<Image <Image
source={require('../assets/images/delete_button.png')} style={styles.button} source={require('../assets/images/Generate.png')} style={{resizeMode:"stretch", height:'85%',aspectRatio: 1,}}
/> />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity onPress={() => { <TouchableOpacity>
addFavourite(trendingMovies[displayIndex]);
}
}>
<Image <Image
source={require('../assets/images/like_button.png')} style={styles.button} source={require('../assets/images/Favorite.png')} style={{ resizeMode:"stretch", height:'65%', aspectRatio: 1,}}
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Timer hours={hours} minutes={minutes} seconds={seconds}></Timer> </SafeAreaView>)}
</>
)
}
type BadgeGenreProps = {
name : String
isSelected: Boolean
}
</SafeAreaView>)} export function BadgeGenre(props: BadgeGenreProps) {
{trendingMovies.length === 0 && ( if(props.isSelected===false){
<SafeAreaView style={styles.background2}> return (
<View style={styles.congratsSection}> <View style={{paddingHorizontal: 20, marginHorizontal: 5,height: 35, backgroundColor: '#2E2E2E', borderRadius: 20, justifyContent: "center"}} >
<Text style={styles.h1}>Félicitations !</Text> <Text style={{color: "white"}}>{props.name}</Text>
<AnimatedLottieView source={require("../assets/animation.json")} autoPlay={true} loop={true} style={{height: 200}}/>
<Text style={styles.explanation}>Vous avez fini la collection du jour.
{"\n"}Revenez à la fin du décompte pour découvrir de nouvelles propositions.</Text>
<Timer2 hours={hours} minutes={minutes} seconds={seconds}></Timer2>
</View> </View>
</SafeAreaView> );
) }
else{
return (
<View style={{paddingHorizontal: 20, marginHorizontal: 5,height: 35, backgroundColor: '#5C5C5C', borderRadius: 20, borderWidth: 1, borderColor: "white" ,justifyContent: "center"}} >
<Text style={{color: "white"}}>{props.name}</Text>
</View>
);
} }
</>
)
} }
type BadgeFilmProps = {
name : String
}
export function BadgeFilm(props: BadgeFilmProps) {
return (
<View style={{
paddingHorizontal: 15,
marginHorizontal: 5,
height: 30,
backgroundColor: '#8906B8',
borderRadius: 15,
justifyContent: "center",
alignSelf: "flex-start"
}}>
<Text style={{color: "white", fontSize: 12, fontWeight: "bold"}}>{props.name}</Text>
</View>
);
}

@ -1,477 +0,0 @@
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>
)
}

@ -1,57 +0,0 @@
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>
);
};

@ -1,51 +1,23 @@
import {FlatList, StyleSheet, View, TextInput, TouchableHighlight} from 'react-native'; import {FlatList, StyleSheet, SafeAreaView, Text, View, Image, TextInput} from 'react-native';
import * as React from "react"; import * as React from "react";
import {BadgeFilm} from "./HomeScreen";
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
import { faClock} from "@fortawesome/free-solid-svg-icons";
import LinearGradient from 'react-native-linear-gradient';
import {RootTabScreenProps} from "../types"; import {RootTabScreenProps} from "../types";
import {useSafeAreaInsets} from "react-native-safe-area-context"; import {useSafeAreaInsets} from "react-native-safe-area-context";
import {useDispatch, useSelector} from 'react-redux'; import {useDispatch,useSelector} from 'react-redux';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {getWatchLater} from "../redux/actions/actions"; import {getTrendingID} from "../redux/actions/actionGetTrendingID";
import Movie from "../model/Movie"; import Movie from "../model/Movie";
import {MovieListComponent} from "../components/MovieListComponent"; export default function WatchLaterScreen({ navigation }: RootTabScreenProps<'WatchLater'>) {
import MovieFinderScreenList from "./MovieFinderScreenList";
export default function WatchLaterScreen({navigation}: RootTabScreenProps<'WatchLater'>) {
const [search, setSearch] = useState('');
const [borderwidth, setBorderWidth] = useState(0);
const [filteredDataSource, setFilteredDataSource] = useState<Movie[]>([]);
const [masterDataSource] = useState([]);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const searchFilterFunction = (text: string) => {
if (text) {
const newData = watchLaterMovies.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);
}
};
// @ts-ignore
const watchLaterMovies = useSelector(state => state.appReducer.watchLaterMovies);
const dispatch = useDispatch();
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
paddingTop: insets.top + 22, paddingTop: 22,
backgroundColor: "#0E0E0E" backgroundColor: "#232323"
}, },
linearGradient: { linearGradient: {
flex: 1, flex: 1,
@ -63,84 +35,98 @@ export default function WatchLaterScreen({navigation}: RootTabScreenProps<'Watch
width: 70, width: 70,
height: 100, height: 100,
borderRadius: 8, 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
}
}); });
const [isLoading, setLoading] = useState(true);
useEffect(() => {
const loadWatchLater = async () => {
// @ts-ignore // @ts-ignore
dispatch(getWatchLater()); const trendingMovies = useSelector(state => state.appReducer.watchLaterMovies);
};
loadWatchLater();
}, [dispatch]);
const ToggleSearchBar = () => {
if (borderwidth === 0)
setBorderWidth(2)
else
setBorderWidth(0)
};
return ( return (
// @ts-ignore <SafeAreaView style={styles.container}>
<MovieFinderScreenList page={"Watch Later"}> <View style={{height: 50, justifyContent: "flex-start",flexDirection: 'row', paddingHorizontal:20, marginBottom: 15,marginVertical:5, alignItems:"flex-end"}} >
<FontAwesomeIcon icon={faClock} style={{marginBottom: -5, marginRight: 20}} size={50} color="white" />
<View style={styles.searchSection}>
<TextInput style={styles.searchBar} onChangeText={(text) => searchFilterFunction(text)} <Text style={{color: "white", fontSize:30}}>Watch Later</Text>
value={search} </View>
placeholder="Rechercher ici..." <Image
placeholderTextColor={"white"} source={require('../assets/images/delimiter.png')} style={{height: 2, width: 400, resizeMode:"stretch"}}
onFocus={ToggleSearchBar} />
onBlur={ToggleSearchBar} <View style={{height:40, width:400, backgroundColor:"grey", borderRadius:20, marginVertical:10, alignSelf:"center"}}>
></TextInput> <TextInput style={{width:'100%', height:40, marginHorizontal:20}} ></TextInput>
</View> </View>
<FlatList <FlatList
data={search.length !== 0 ? filteredDataSource : watchLaterMovies} data={trendingMovies}
keyExtractor={item => item.original_title} keyExtractor={item => item.original_title}
// @ts-ignore renderItem={({item}) => <ListWidget movie={item} ></ListWidget>}
renderItem={({item}) => <TouchableHighlight onPress={() => navigation.navigate("Info", {"item": item})}><MovieListComponent movie={item}></MovieListComponent></TouchableHighlight>}
/> />
</MovieFinderScreenList> </SafeAreaView>
); );
} }
type ListWidgetProps = {
movie : Movie
}
export function ListWidget(props: ListWidgetProps) {
const insets = useSafeAreaInsets();
const styles = StyleSheet.create({
filmCard: {
width: 70,
height: 100,
borderRadius: 8,
},
});
function formatTime(time: number) {
console.log(time);
const hours = Math.floor(time / 60);
const minutes = time % 60;
return `${hours}h ${minutes < 10 ? `0${minutes}` : minutes}m`;
}
return (
<View style={{
height: 100,
borderRadius: 20,
justifyContent: "flex-start",
flexDirection: 'row',
paddingHorizontal: 20,
marginVertical: 5
}}>
<Image
style={styles.filmCard}
source={{
uri: props.movie.poster_path,
}}
/>
<View style={{
height: 100,
borderRadius: 20,
justifyContent: "flex-start",
flexDirection: 'column',
paddingLeft: 10
}}>
<Text numberOfLines={1} style={{
color: "white",
fontWeight: "bold",
fontSize: 25,
paddingRight: 50
}}>{props.movie.original_title}</Text>
<Text style={{color: "grey", fontWeight: "bold", fontSize: 17}}>{formatTime(props.movie.runtime)}</Text>
<View style={{marginVertical: 10}}>
<BadgeFilm name={"Science-Ficton"}/>
</View>
</View>
</View>
);
}

@ -1,68 +0,0 @@
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)
}
}

@ -3,14 +3,13 @@
* https://reactnavigation.org/docs/typescript/ * https://reactnavigation.org/docs/typescript/
*/ */
import {BottomTabScreenProps} from '@react-navigation/bottom-tabs'; import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import {CompositeScreenProps, NavigatorScreenParams} from '@react-navigation/native'; import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';
import {NativeStackScreenProps} from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
declare global { declare global {
namespace ReactNavigation { namespace ReactNavigation {
interface RootParamList extends RootStackParamList { interface RootParamList extends RootStackParamList {}
}
} }
} }
@ -22,8 +21,6 @@ export type RootStackParamList = {
WatchLater: undefined; WatchLater: undefined;
Favorite: undefined; Favorite: undefined;
Info: undefined;
}; };
@ -36,8 +33,6 @@ export type RootTabParamList = {
WatchLater: undefined; WatchLater: undefined;
Home: undefined; Home: undefined;
Favorite: undefined; Favorite: undefined;
Info: undefined;
}; };
export type RootTabScreenProps<Screen extends keyof RootTabParamList> = CompositeScreenProps< export type RootTabScreenProps<Screen extends keyof RootTabParamList> = CompositeScreenProps<

Loading…
Cancel
Save