Compare commits

...

118 Commits

Author SHA1 Message Date
Lucas DELANIER efcc85145d Mise à jour de 'README.md'
2 years ago
Louison PARANT 9e56b43c3f Mise à jour de 'README.md'
2 years ago
Lucas Delanier b2781a9f50 Merge branch 'Tests'
2 years ago
Lucas Delanier b8fda522d7 last test
2 years ago
Lucas DELANIER 429c887ecb Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 17792a947d Transférer les fichiers vers 'Documentation'
2 years ago
Louison PARANT 02a421e125 Tests (Failed Test Suite)
2 years ago
Louison PARANT 69028b929b Tests (Failed Test Suite)
2 years ago
Lucas Delanier eefc7270b5 delete useless reducers
2 years ago
Lucas DELANIER e019b79824 Mise à jour de 'package.json'
2 years ago
Lucas DELANIER 3558e40561 Mise à jour de 'package.json'
2 years ago
Louison PARANT 9d0e2082ad Merge remote-tracking branch 'origin/master'
2 years ago
Lucas Delanier 51c95d68ef test on actions 🎉
2 years ago
Louison PARANT c3b7895479 Test Reducers
2 years ago
Lucas Delanier b18674d801 Merge remote-tracking branch 'origin/master'
2 years ago
Lucas Delanier e1405d8b9e test on actions 🎉
2 years ago
Lucas DELANIER d37db641ad Mise à jour de '.drone.yml'
2 years ago
Lucas Delanier 7eb142edb7 Merge remote-tracking branch 'origin/master'
2 years ago
Lucas Delanier 6cd504b41b test on actions 🎉
2 years ago
Lucas Delanier 7628844687 test on actions 🎉
2 years ago
Louison PARANT 41d0ff25bd Change files name (tsx->ts)
2 years ago
Louison PARANT 710add175a Await Code Smell Fix (FavouriteScreen+WatchLaterScreen)
2 years ago
Louison PARANT ada566c25c Null List Error Fix (+Favourite Page Name)
2 years ago
Lucas DELANIER 15e4b2467e Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 8949b25507 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER ef1bd8d4cc Ajouter 'jest.config.ts'
2 years ago
Lucas DELANIER 53dbab2176 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER e00a43d84d Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 8a36511465 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 7d07f20298 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 90dfc1bfa4 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 635baef2de Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER fdbc7c5b7f Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 5449ce6534 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER abcbb391d6 Mise à jour de '.drone.yml'
2 years ago
Lucas Delanier 1ce81289c8 first Jest test 💫
2 years ago
Lucas Delanier ed46cec5a7 Merge remote-tracking branch 'origin/master'
2 years ago
Lucas Delanier 32692147db Addind child props for list pages 💫
2 years ago
Lucas DELANIER 4014a4ec83 Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER f382785fe7 Mise à jour de 'README.md'
2 years ago
Lucas Delanier 4248e0de09 clean code smells 👮
2 years ago
Lucas Delanier eef6737ac9 clean code smells 👮
2 years ago
Lucas Delanier 9a577959af clean code smells 👮
2 years ago
Louison PARANT ef5c4f1eb1 Code Smell Corrections
2 years ago
Lucas DELANIER 2a5e43ded9 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 60d70ea206 Ajouter '.drone.yml'
2 years ago
Lucas Delanier bae57f3769 fix problem last movie from lists not register 🍻
2 years ago
Lucas Delanier 9c21de819b add persistencea
2 years ago
Lucas DELANIER cb82e9cc21 Supprimer 'sonar-project.properties'
2 years ago
Lucas DELANIER 6963f37d87 Supprimer '.github/workflows/build.yml'
2 years ago
Lucas DELANIER ca8bbe546f Supprimer '.drone.yml'
2 years ago
Lucas DELANIER e10e42a220 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER e19eb577ab Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 2e2f18ad49 Mise à jour de '.drone.yml'
2 years ago
Lucas DELANIER 22c30b0765 Ajouter '.drone.yml'
2 years ago
Lucas Delanier 333db70258 fix card
2 years ago
Lucas DELANIER 4b2e5d62bd Mise à jour de 'README.md'
2 years ago
Lucas DELANIER d2cff1d14c Ajouter '.github/workflows/build.yml'
2 years ago
Lucas DELANIER c045947644 Ajouter 'sonar-project.properties'
2 years ago
Lucas DELANIER 614fbb37c4 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 5334d2f0e7 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER a13a3c0c8c Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 228dc121ff Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER c3257d3a87 Supprimer 'Documentation/banner_image.png'
2 years ago
Lucas DELANIER 799f8f8afe Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER 5662ab3795 Supprimer 'Documentation/banner_image.png'
2 years ago
Lucas DELANIER 20f9651c89 Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER 764cde3984 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER b65a0e5c41 Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER ee602fadc3 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 26038b24e8 Supprimer 'Documentation/test'
2 years ago
Lucas DELANIER 7b07047d90 Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER 037e561598 Supprimer 'Documentation/Group 1 (10).png'
2 years ago
Lucas DELANIER 53eb066e77 Transférer les fichiers vers 'Documentation'
2 years ago
Lucas DELANIER 424c5ecd97 Ajouter 'Documentation/test'
2 years ago
Lucas Delanier ebfd41854d Merge remote-tracking branch 'origin/master'
2 years ago
Lucas Delanier 16fd168c8b commit new "Coup de Coeur" and "Nouveau" card displayed on the Swipeable card link with our new API 🎉🎉
2 years ago
Lucas DELANIER 3b84da58f1 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER a66c987f20 Mise à jour de 'README.md'
2 years ago
Louison PARANT 95d4d60a87 Merge remote-tracking branch 'origin/master'
2 years ago
Louison PARANT 40ffb55f8f Local Storage (get/set)
2 years ago
Louison PARANT aedb52680b Mise à jour de 'README.md'
2 years ago
Louison PARANT b539572de0 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 52e7b119d8 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 07d49e514e Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 037c376443 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 8a5ea703b8 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER a05cfe90e1 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER e47b11456e Mise à jour de 'README.md'
2 years ago
Lucas DELANIER f70850eeb0 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER b5f381218f Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 087756eefc Mise à jour de 'README.md'
2 years ago
Lucas DELANIER af1ab5915e Mise à jour de 'README.md'
2 years ago
Lucas DELANIER 8abb49df94 Mise à jour de 'README.md'
2 years ago
Lucas DELANIER be12b66d7a Mise à jour de 'README.md'
2 years ago
Lucas DELANIER cb2991760a Ajouter 'README.md'
2 years ago
Lucas Delanier 3892598991 creation formatTime function 🚑
2 years ago
Lucas Delanier 221fcd4d31 fix profile picture 🚑
2 years ago
Lucas Delanier a94135f17d fix icon size 🚑
2 years ago
Lucas Delanier 6340ff89e8 Better search bar UX 🎨
2 years ago
Lucas Delanier f0154a514c Code clean 🎉
2 years ago
Lucas Delanier b80ad6d113 fix review and better look
2 years ago
Lucas Delanier 62584127bc add review and recommendations to info page
2 years ago
Lucas Delanier 5dd284539c info page
2 years ago
Lucas Delanier c85637aacd link action like with button
2 years ago
Louison PARANT 71d4de0057 Favourite Movies
2 years ago
Louison PARANT 3d67df8e05 Favourite Movies
2 years ago
Louison PARANT c6cfe46e7b Favourite Movies
2 years ago
Lucas Delanier 4cfb2c296d fix problem when handle last card of the list
2 years ago
Lucas Delanier e09917e2d5 new ui likes and page when no cards
2 years ago
Lucas Delanier 5d9ac42909 new ui , search in list, and vote
2 years ago
Lucas Delanier 821a9e5903 add resume to movies
2 years ago
Lucas Delanier 4987e8fbb8 add resume to movies
2 years ago
Lucas Delanier a5fcfbca85 display genres and create movie with their genres
2 years ago
Lucas Delanier 0202f27b80 fix display homepage
2 years ago
Lucas Delanier f243584049 fix display homepage
2 years ago
Lucas Delanier 39ff985d99 push homescreen and watch later display
2 years ago
Lucas Delanier 59b584e55d test redux on homepage
2 years ago

@ -0,0 +1,19 @@
kind: pipeline
type: docker
name: default
steps:
- name: sonar-analyses
image: hub.codefirst.iut.uca.fr/camille.petitalot/drone-sonarplugin-reactnative:latest
commands:
- npm install
- npm run test
- ls ./components/__tests__/
- sonar-scanner -Dsonar.projectKey=MovieFinder -Dsonar.sources=. -Dsonar.host.url=$${PLUGIN_SONAR_HOST}
-Dsonar.login=$${PLUGIN_SONAR_TOKEN} -Dsonar.javascript.lcov.reportPaths=./components/__tests__/lcov.info
-Dsonar.exclusions=**/lcov-report/**
secrets: [ SONAR_TOKEN ]
settings:
sonar_host: https://codefirst.iut.uca.fr/sonar/
sonar_token:
from_secret: SONAR_TOKEN

@ -1,22 +1,25 @@
import { StatusBar } from 'expo-status-bar'; import {StatusBar} from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'; import {SafeAreaProvider} from 'react-native-safe-area-context'
import useCachedResources from './hooks/useCachedResources'; import useCachedResources from './hooks/useCachedResources'
import useColorScheme from './hooks/useColorScheme'; import useColorScheme from './hooks/useColorScheme'
import Navigation from './navigation'; import Navigation from './navigation'
import {View} from "react-native"; import store from "./redux/store"
import {Provider} from "react-redux"
export default function App() { export default function App() {
const isLoadingComplete = useCachedResources(); const isLoadingComplete = useCachedResources()
const colorScheme = useColorScheme(); const colorScheme = useColorScheme()
if (!isLoadingComplete) { if (!isLoadingComplete) {
return null; return null
} else { } else {
return ( return (
<SafeAreaProvider> <Provider store={store}>
<Navigation colorScheme={colorScheme} /> <SafeAreaProvider>
<StatusBar /> <Navigation colorScheme={colorScheme}/>
</SafeAreaProvider> <StatusBar/>
); </SafeAreaProvider>
} </Provider>
);
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

@ -0,0 +1,89 @@
<p align="center">
<img src="https://codefirst.iut.uca.fr/git/lucas.delanier/MovieFinder/raw/branch/master/Documentation/banner_image.png " />
</p>
![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.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 561 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,6 +1,9 @@
module.exports = function(api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: ['babel-preset-expo'] presets: ['babel-preset-expo'],
}; plugins: [
'react-native-reanimated/plugin',
]
};
}; };

@ -0,0 +1,37 @@
import {StyleSheet, Text, View} from "react-native";
import * as React from "react";
import Stars from "./StarsComponent";
import Movie from "../model/Movie";
import {formatTime} from "../model/formatTime";
type headerMovieProps = {
movie: Movie
}
export function HeaderMovie(props: headerMovieProps) {
const styles = StyleSheet.create({
circle: {
width: 6,
height: 6,
borderRadius: 100 / 2,
marginTop: 4,
backgroundColor: "lightgray",
marginHorizontal: 8,
},
});
return (<View style={{flexDirection: 'column', alignSelf: 'center', paddingHorizontal: 30, width: '100%', alignItems: "center", paddingTop: 10, flex: 0.07}}>
<Text numberOfLines={1} style={{color: "white", fontSize: 30, fontWeight: "bold", paddingTop: 5, alignSelf: "center"}}>{props.movie.original_title}</Text>
<View style={{flexDirection: 'row', justifyContent: "center", alignItems: "center", alignSelf: "center"}}>
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${props.movie.release_date}`}</Text>
<View style={styles.circle}/>
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${props.movie.genres[0]} ${props.movie.genres[1] !== undefined ? ", " + props.movie.genres[1] : ""}`}</Text>
<View style={styles.circle}/>
<Text style={{color: "#D1D1D1", fontSize: 20, fontWeight: "normal", paddingTop: 5}}>{`${formatTime(props.movie.runtime)}`}</Text>
</View>
<Stars note={props.movie.vote_average} size={110}/>
</View>);
}

@ -0,0 +1,98 @@
import Movie from "../model/Movie.js";
import {Image, StyleSheet, Text, View} from "react-native";
import {LinearGradient} from "expo-linear-gradient";
import Stars from "./StarsComponent";
import * as React from "react";
import {formatTime} from "../model/formatTime";
type MovieListProps = {
movie: Movie
}
export function MovieListComponent(props: MovieListProps) {
const styles = StyleSheet.create({
filmCard: {
width: 70,
height: 110,
borderRadius: 8,
},
body: {
height: 130,
borderRadius: 20,
justifyContent: "flex-start",
flexDirection: 'row',
marginHorizontal: 10,
marginVertical: 7,
paddingHorizontal: 10,
backgroundColor: "#1D1D1D",
alignItems: "center",
borderWidth: 1.5,
borderColor: "#1F1F1F"
},
section: {
height: 130,
width: "85%",
justifyContent: "center",
flexDirection: 'column',
paddingRight: 20,
paddingLeft: 20,
},
h1: {
color: "white",
fontWeight: "700",
fontSize: 21,
},
infoSection: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%"
},
top: {
flexDirection: "row",
alignItems: "center"
},
h3: {
color: "grey",
fontWeight: "600"
},
vote: {
paddingLeft: 7,
color: "white",
fontWeight: "bold",
fontSize: 13
}
});
return (
<LinearGradient style={styles.body} start={{x: 0, y: 1}}
end={{x: 1, y: 1}}
// Button Linear Gradient
colors={['#0B0B0B', '#1F1F1F']}>
<Image
style={styles.filmCard}
source={{
uri: props.movie.poster_path_min,
}}
/>
<View style={styles.section}>
<Text numberOfLines={1} style={styles.h1}>{props.movie.original_title}</Text>
<View style={styles.infoSection}>
<View style={styles.top}>
<Stars note={props.movie.vote_average} size={90}></Stars>
<Text style={styles.vote}>{props.movie.vote_average.toFixed(1)}</Text>
</View>
<Text style={styles.h3}>{formatTime(props.movie.runtime)}</Text>
</View>
<Text numberOfLines={3} style={{color: "#C7C7C7", fontWeight: "600",}}>{props.movie.overview}</Text>
</View>
</LinearGradient>
);
}

@ -0,0 +1,46 @@
import {Image, View} from "react-native";
import * as React from "react";
type StarsProps = {
note: number
size: number
}
export function Stars(props: StarsProps) {
let imageSource;
let note = props.note / 2;
if (note < 0.5)
imageSource = require('../assets/images/0.5stars_vote.png');
else if (note < 1)
imageSource = require('../assets/images/1stars_vote.png');
else if (note < 1.5)
imageSource = require('../assets/images/1.5stars_vote.png');
else if (note < 2)
imageSource = require('../assets/images/2stars_vote.png');
else if (note < 2.5)
imageSource = require('../assets/images/2.5stars_vote.png');
else if (note < 3)
imageSource = require('../assets/images/3stars_vote.png');
else if (note < 3.5)
imageSource = require('../assets/images/3.5stars_vote.png');
else if (note < 4)
imageSource = require('../assets/images/4stars_vote.png');
else if (note < 4.5)
imageSource = require('../assets/images/4.5stars_vote.png');
else if (note < 5)
imageSource = require('../assets/images/5stars_vote.png');
return (
<View>
<Image source={imageSource} style={{
width: props.size,
height: 40,
resizeMode: 'contain'
}}/>
</View>
);
};
export default Stars;

@ -0,0 +1,44 @@
import {Image, Text, View} from "react-native";
import * as React from "react";
type TimerProps = {
hours: number
minutes: number
seconds: number
}
export function Timer(props: TimerProps) {
return (
<View style={{zIndex: 1, alignContent: "center", justifyContent: "center", flex: 0.15, flexDirection: "row"}}>
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>Nouvelle collection dans</Text>
<Image source={require('../assets/images/timer_icon.png')} style={{
height: 30,
resizeMode: 'contain',
top: -5
}}></Image>
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.hours.toString().padStart(2, '0')}:`}</Text>
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.minutes.toString().padStart(2, '0')}:`}</Text>
<Text style={{color: "#FFF", fontSize: 16, fontWeight: "500"}}>{`${props.seconds.toString().padStart(2, '0')}`}</Text>
</View>
);
}
export function Timer2(props: TimerProps) {
return (
<View style={{zIndex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 20, paddingVertical: 10, borderRadius: 100, flexDirection: "row", bottom: 0, backgroundColor: "white", marginTop: 50}}>
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>Nouvelle collection dans</Text>
<Image source={require('../assets/images/timer_icon2.png')} style={{
height: 30,
resizeMode: 'contain', marginHorizontal: 7
}}></Image>
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.hours.toString().padStart(2, '0')}:`}</Text>
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.minutes.toString().padStart(2, '0')}:`}</Text>
<Text style={{color: "black", fontSize: 16, fontWeight: "500"}}>{`${props.seconds.toString().padStart(2, '0')}`}</Text>
</View>
);
}

@ -1,10 +0,0 @@
import * as React from 'react';
import renderer from 'react-test-renderer';
import { MonoText } from '../StyledText';
it(`renders correctly`, () => {
const tree = renderer.create(<MonoText>Snapshot test!</MonoText>).toJSON();
expect(tree).toMatchSnapshot();
});

@ -0,0 +1,216 @@
import Movie from "../../model/Movie"
import {describe, expect} from '@jest/globals'
import {addMovieToFavourite, addMovieToWatchLater, removeMovieTrending, setFavourite, setinfoMovie, setWatchLater} from "../../redux/actions/actions"
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from "../../redux/constants"
describe('test actions add WatchLater', () => {
it('should create an action with ADD_WATCHLATER type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: ADD_WATCHLATER,
payload: payload
};
expect(addMovieToWatchLater(payload)).toEqual(expectation);
});
})
describe('test actions add Favourite', () => {
it('should create an action with ADD_FAVOURITE type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: ADD_FAVOURITE,
payload: payload
};
expect(addMovieToFavourite(payload)).toEqual(expectation);
});
})
describe('test actions load watchlater', () => {
it('should create an action with ADD_FAVOURITE type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: LOAD_WATCHLATER,
payload: [payload]
};
expect(setWatchLater([payload])).toEqual(expectation);
});
})
describe('test actions load favourite', () => {
it('should create an action with LOAD_FAVOURITE type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: LOAD_FAVOURITE,
payload: [payload]
};
expect(setFavourite([payload])).toEqual(expectation);
});
})
describe('test actions load info movies', () => {
it('should create an action with FETCH_TRENDING_MOVIE type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: FETCH_TRENDING_MOVIE,
payload: [payload]
};
expect(setinfoMovie([payload])).toEqual(expectation);
});
})
describe('test actions remove movies', () => {
it('should create an action with removeMovieTrending type', () => {
const payload = new Movie(
916224,
"Suzume",
"https://image.tmdb.org/t/p/original/ceYZCBfwbBwSpGJ6PapNVw5jqLG.jpg",
121,
8.311,
"2022-11-11",
[
"Animation",
"Drame",
"Aventure",
"Fantastique",
],
"Dans une petite ville paisible de Kyushu, une jeune fille de 17 ans, Suzume, rencontre un homme qui dit voyager afin de chercher une porte. Décidant de le suivre dans les montagnes, elle découvre une unique porte délabrée trônant au milieu des ruines, seul vestige ayant survécu au passage du temps. Cédant à une inexplicable impulsion, Suzume tourne la poignée, et d'autres portes s'ouvrent alors aux quatre coins du Japon, laissant entrer toutes les catastrophes qu'elles renferment. L'homme est formel : toute porte ouverte doit être fermée. Là où elle s'est égarée se trouvent les étoiles, le crépuscule et l'aube, une voûte céleste où tous les temps se confondent. Guidée par des portes nimbées de mystère, Suzume entame un périple en vue de toutes les refermer.",
"https://image.tmdb.org/t/p/w780/hOJYwkVSgXtE3BJFN0bRPKdLJLj.jpg",
)
const expectation = {
type: POP_FIRST_TRENDING,
payload: payload
};
expect(removeMovieTrending(payload)).toEqual(expectation);
});
})

@ -0,0 +1,172 @@
import Movie from "../../model/Movie"
import {describe, expect} from '@jest/globals'
import appReducer from "../../redux/reducers/appReducer"
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from "../../redux/constants"
describe('test null', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should return initial state', () => {
expect(appReducer(undefined, {})).toEqual(initialState)
})
})
describe('test ADD_FAVOURITE', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle ADD_FAVOURITE', () => {
const favourite = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
expect(
appReducer(initialState, {
type: ADD_FAVOURITE,
payload: favourite,
})
).toEqual({
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [favourite, ...initialState.favouriteMovies],
})
})
})
describe('test ADD_WATCHLATER', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle ADD_WATCHLATER', () => {
const watchLater = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
expect(
appReducer(initialState, {
type: ADD_WATCHLATER,
payload: watchLater,
})
).toEqual({
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [watchLater, ...initialState.watchLaterMovies],
favouriteMovies: [],
})
})
})
describe('test LOAD_WATCHLATER', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle LOAD_WATCHLATER', () => {
const watchLater = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
const MovieList = [watchLater]
expect(
appReducer(initialState, {
type: LOAD_WATCHLATER,
payload: MovieList,
})
).toEqual({
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [watchLater, ...initialState.watchLaterMovies],
favouriteMovies: [],
})
})
})
describe('test LOAD_FAVOURITE', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle LOAD_FAVOURITE', () => {
const favourite = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
const MovieList = [favourite]
expect(
appReducer(initialState, {
type: LOAD_FAVOURITE,
payload: MovieList,
})
).toEqual({
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [favourite, ...initialState.favouriteMovies],
})
})
})
describe('test FETCH_TRENDING_MOVIE', () => {
let initialState = {
trendingIDs: [],
trendingMovies: [],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle FETCH_TRENDING_MOVIE', () => {
const trending = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
const MovieList = [trending]
expect(
appReducer(initialState, {
type: FETCH_TRENDING_MOVIE,
payload: MovieList,
})
).toEqual({
trendingIDs: [],
trendingMovies: [trending, ...initialState.trendingMovies],
watchLaterMovies: [],
favouriteMovies: [],
})
})
})
describe('test POP_FIRST_TRENDING', () => {
const trending = new Movie(1, "Test", "", 1, 5, "2023", ["Halloween"], "", "")
let initialState = {
trendingIDs: [],
trendingMovies: [trending],
watchLaterMovies: [],
favouriteMovies: [],
}
it('should handle POP_FIRST_TRENDING', () => {
expect(
appReducer(initialState, {
type: POP_FIRST_TRENDING,
payload: trending,
})
).toEqual({
trendingIDs: [],
trendingMovies: [...initialState.trendingMovies.filter((item: Movie) => item !== trending)],
watchLaterMovies: [],
favouriteMovies: [],
})
})
})

@ -0,0 +1,32 @@
import {describe, expect, test} from '@jest/globals';
import {formatTime} from "../../model/formatTime";
describe('return formated time', () => {
test('125to 2h 05m', () => {
expect(formatTime(125)).toBe("2h 05m");
});
test('45to 0h 45m', () => {
expect(formatTime(45)).toBe("0h 45m");
});
test('203to 3h 23m', () => {
expect(formatTime(203)).toBe("3h 23m");
});
});
/*
"verbose": true,
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
],
"testMatch": [
"**.test.js"
],
"testEnvironment": "node",
"testEnvironmentOptions": {
"browsers": [
"chrome",
"firefox",
"safari"
]
}
*/

@ -0,0 +1,27 @@
import {Image, View} from "react-native";
import * as React from "react";
export function SuggestedCard() {
return (
<View>
<Image style={{height: 28, width: 152, marginVertical: 5}}
source={require('../assets/images/suggested_card.png')}
/>
</View>
);
}
export function NewCard() {
return (
<View>
<Image style={{height: 28, width: 99, marginVertical: 5}}
source={require('../assets/images/new_card.png')}
/>
</View>
);
}

@ -0,0 +1,4 @@
export default {
api_key: "a133422b5b1f22428e8074470d321865",
base_url: "https://api.themoviedb.org/3/"
}

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

@ -0,0 +1,16 @@
class MinimalMovie {
public original_title: string
public poster_path: string
constructor(original_title: string, poster_path: string) {
this.original_title = original_title;
this.poster_path = 'https://image.tmdb.org/t/p/w185' + poster_path;
}
}
export default MinimalMovie;

@ -0,0 +1,33 @@
class Movie {
public poster_path_min : string
public full_date : string
constructor(
public id: number,
public original_title: string,
public poster_path: string,
public runtime: number,
public vote_average: number,
public release_date: string,
public genres: string[],
public overview: string,
public backdrop_path: string
) {
this.id = id;
this.original_title = original_title;
this.poster_path = 'https://image.tmdb.org/t/p/w780' + poster_path;
this.poster_path_min = 'https://image.tmdb.org/t/p/w185' + poster_path;
this.runtime = runtime;
this.release_date = release_date.substring(0, 4);
this.full_date = release_date;
this.genres = genres;
this.overview = overview;
this.vote_average = vote_average;
this.backdrop_path = 'https://image.tmdb.org/t/p/original' + backdrop_path;
}
}
export default Movie;

@ -0,0 +1,6 @@
export function formatTime(time: number) {
const hours = Math.floor(time / 60);
const minutes = time % 60;
const minutesToDisplay = minutes < 10 ? `0${minutes}` : minutes
return `${hours}h ${minutesToDisplay}m`;
}

@ -0,0 +1,28 @@
class Review {
public message: string
public pseudo: string
public profile_path: string
public date: string
constructor(message: string, profile_path: string, date: string, pseudo: string) {
this.message = message;
if (profile_path == null) {
this.profile_path = "https://thumbs.dreamstime.com/b/profil-vectoriel-avatar-par-d%C3%A9faut-utilisateur-179376714.jpg";
} else if (profile_path.slice(0, 6) === "/https") {
this.profile_path = profile_path.slice(1, 100);
} else {
this.profile_path = 'https://image.tmdb.org/t/p/w185' + profile_path;
}
this.date = date.substring(0, 10);
this.pseudo = pseudo;
}
}
export default Review;

@ -4,38 +4,44 @@
* 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('/')],
config: { config: {
screens: {
Root: {
screens: { screens: {
WatchLater: { Root: {
screens: { screens: {
WatchLaterScreen: 'WatchLater', WatchLater: {
}, screens: {
}, WatchLaterScreen: 'WatchLater',
Home: { },
screens: { },
HomeScreen: 'Home', Home: {
}, screens: {
}, HomeScreen: 'Home',
Favorite: { },
screens: { },
FavoriteScreen: 'Favorite', Favorite: {
screens: {
FavoriteScreen: 'Favorite',
},
},
Info: {
screens: {
FavoriteScreen: 'InfoScreen',
},
},
},
}, },
}, Modal: 'modal',
NotFound: '*',
}, },
},
Modal: 'modal',
NotFound: '*',
}, },
},
}; };
export default linking; export default linking;

@ -1,110 +1,146 @@
/** import {FontAwesomeIcon} from "@fortawesome/react-native-fontawesome"
* If you are not familiar with React Navigation, refer to the "Fundamentals" guide: import {faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons"
* https://reactnavigation.org/docs/getting-started import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'
* import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native'
*/ import {createNativeStackNavigator} from '@react-navigation/native-stack'
import { FontAwesome } from '@expo/vector-icons'; import * as React from 'react'
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome"; import {ColorSchemeName} from 'react-native'
import { faClock, faFilm, faHeart} from "@fortawesome/free-solid-svg-icons"; import useColorScheme from '../hooks/useColorScheme'
import Ionicons from '@expo/vector-icons/Ionicons'; import NotFoundScreen from '../screens/NotFoundScreen'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import WatchLaterScreen from '../screens/WatchLaterScreen'
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; import FavoriteScreen from '../screens/FavoriteScreen'
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import HomeScreen from '../screens/HomeScreen'
import * as React from 'react'; import InfoScreen from '../screens/InfoScreen'
import { ColorSchemeName, Pressable } from 'react-native'; import {RootStackParamList, RootTabParamList, RootTabScreenProps} from '../types'
import LinkingConfiguration from './LinkingConfiguration'
import Colors from '../constants/Colors'; import {useCallback, useEffect, useState} from "react"
import useColorScheme from '../hooks/useColorScheme'; import {useDispatch} from "react-redux"
import NotFoundScreen from '../screens/NotFoundScreen'; import {getTrendingID, loadWatchLater} from "../redux/actions/actions"
import WatchLaterScreen from '../screens/WatchLaterScreen'; import * as SplashScreen from 'expo-splash-screen'
import FavoriteScreen from '../screens/FavoriteScreen';
import HomeScreen from '../screens/HomeScreen'; export default function Navigation({colorScheme}: { colorScheme: ColorSchemeName }) {
import { RootStackParamList, RootTabParamList, RootTabScreenProps } from '../types'; const [appIsReady, setAppIsReady] = useState(false)
import LinkingConfiguration from './LinkingConfiguration'; const dispatch = useDispatch()
useEffect(() => {
export default function Navigation({ colorScheme }: { colorScheme: ColorSchemeName }) {
return ( async function prepare() {
<NavigationContainer try {
linking={LinkingConfiguration} const loadTrendingID = async () => {
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> // @ts-ignore
<RootNavigator /> dispatch(getTrendingID())
</NavigationContainer> };
); loadTrendingID()
const list = dispatch(loadWatchLater())
loadWatchLater(list)
} catch (e) {
} finally {
// Tell the application to render
setAppIsReady(true)
}
}
prepare();
}, []);
useCallback(async () => {
if (appIsReady) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync()
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<NavigationContainer
linking={LinkingConfiguration}
theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<RootNavigator/>
</NavigationContainer>
);
} }
/** /**
* A root stack navigator is often used for displaying modals on top of all other content. * A root stack navigator is often used for displaying modals on top of all other content.
* https://reactnavigation.org/docs/modal * https://reactnavigation.org/docs/modal
*/ */
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>()
function RootNavigator() { function RootNavigator() {
return ( return (
<Stack.Navigator> <Stack.Navigator>
<Stack.Screen name="Root" component={BottomTabNavigator} options={{ headerShown: false }} /> <Stack.Screen name="Root" component={BottomTabNavigator} options={{headerShown: false}}/>
<Stack.Screen name="Home" component={HomeScreen} options={{ headerShown: false }} /> <Stack.Screen name="Home" component={HomeScreen} options={{headerShown: false}}/>
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} /> <Stack.Screen name="NotFound" component={NotFoundScreen} options={{title: 'Oops!'}}/>
<Stack.Screen name="Favorite" component={FavoriteScreen} options={{ headerShown: false }} /> <Stack.Screen name="Favorite" component={FavoriteScreen} options={{headerShown: false}}/>
<Stack.Screen name="WatchLater" component={WatchLaterScreen} options={{ headerShown: false }} /> <Stack.Screen name="WatchLater" component={WatchLaterScreen} options={{headerShown: false}}/>
</Stack.Navigator> <Stack.Screen name="Info" component={InfoScreen} options={{headerShown: false}}/>
); </Stack.Navigator>
);
} }
/** /**
* A bottom tab navigator displays tab buttons on the bottom of the display to switch screens. * A bottom tab navigator displays tab buttons on the bottom of the display to switch screens.
* https://reactnavigation.org/docs/bottom-tab-navigator * https://reactnavigation.org/docs/bottom-tab-navigator
*/ */
const BottomTab = createBottomTabNavigator<RootTabParamList>(); const BottomTab = createBottomTabNavigator<RootTabParamList>()
function BottomTabNavigator() { function BottomTabNavigator() {
const colorScheme = useColorScheme(); let colorScheme = useColorScheme()
const isLightTheme = colorScheme === "light"
return (
<BottomTab.Navigator return (
initialRouteName="WatchLater" <BottomTab.Navigator
screenOptions={{ initialRouteName="Home"
tabBarActiveTintColor: "purple", screenOptions={{
tabBarActiveTintColor: isLightTheme ? "black" : "white",
}}>
<BottomTab.Screen }}>
name="WatchLater" <BottomTab.Screen
name="WatchLater"
component={WatchLaterScreen}
options={({ navigation }: RootTabScreenProps<'WatchLater'>) => ({ component={WatchLaterScreen}
tabBarIcon: ({ color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>, options={({navigation}: RootTabScreenProps<'WatchLater'>) => ({
headerShown: false, tabBarIcon: ({color, size}) => <TabBarIcon name={faClock} color={color} size={20}/>,
headerShown: false,
})}
/> })}
<BottomTab.Screen />
name="Home" <BottomTab.Screen
component={HomeScreen} name="Home"
component={HomeScreen}
options={{
headerShown: false, options={{
tabBarIcon: ({ color, size }) => <TabBarIcon name={faFilm} color={color} size={20}/>, headerShown: false,
}} tabBarIcon: ({color, size}) => <TabBarIcon name={faFilm} color={color} size={20}/>,
/> }}
<BottomTab.Screen />
name="Favorite" <BottomTab.Screen
component={FavoriteScreen} name="Favorite"
component={FavoriteScreen}
options={{
headerShown: false, options={{
tabBarIcon: ({ color, size }) => <TabBarIcon name={faHeart} color={color} size={20} />, headerShown: false,
}} tabBarIcon: ({color, size}) => <TabBarIcon name={faHeart} color={color} size={20}/>,
/> }}
</BottomTab.Navigator> />
); </BottomTab.Navigator>
);
} }
/** /**
* You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ * You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
*/ */
function TabBarIcon(props: { function TabBarIcon(props: {
name: any; name: any;
color: string; color: string;
size: number; size: number;
}) { }) {
return <FontAwesomeIcon icon={props.name} style={{marginBottom: -5}} size={props.size} color={props.color} />; return <FontAwesomeIcon icon={props.name} style={{marginBottom: -5}} size={props.size} color={props.color}/>;
} }

36055
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -7,10 +7,12 @@
"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 --watchAll" "test": "jest --coverage --coverageDirectory=./components/__tests__"
}, },
"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",
@ -19,10 +21,15 @@
"@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",
@ -33,25 +40,40 @@
"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.7.0", "react-native-svg": "13.4.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": "^26.6.3", "jest": "^29.5.0",
"jest-expo": "~47.0.1", "jest-expo": "~47.0.1",
"react-test-renderer": "18.1.0", "react-test-renderer": "18.1.0",
"typescript": "^4.6.3" "ts-jest": "^29.0.5",
"typescript": "^4.9.5"
}, },
"private": true "private": true
} }

Binary file not shown.

@ -0,0 +1,124 @@
import {ADD_FAVOURITE, ADD_WATCHLATER, FETCH_TRENDING_MOVIE, LOAD_FAVOURITE, LOAD_WATCHLATER, POP_FIRST_TRENDING} from '../constants'
import config from "../../constants/config"
import Movie from "../../model/Movie"
import {getFavouriteList, getWatchLaterList} from "../../storage/storage"
export const setWatchLater = (TrendingMovieList: null | Movie[]) => {
return {
type: LOAD_WATCHLATER,
payload: TrendingMovieList,
}
}
export const setFavourite = (FavouriteList: null | Movie[]) => {
return {
type: LOAD_FAVOURITE,
payload: FavouriteList,
}
}
export const getWatchLater = () => {
// @ts-ignore
return async dispatch => {
try {
let MovieList = await getWatchLaterList();
dispatch(setWatchLater(MovieList));
} catch (error) {
console.log('Error', error);
}
}
}
export const getFavourite = () => {
// @ts-ignore
return async dispatch => {
try {
let MovieList = await getFavouriteList();
dispatch(setFavourite(MovieList));
} catch (error) {
console.log('Error', error);
}
}
}
export const setinfoMovie = (TrendingMovieList: null | Movie[]) => {
return {
type: FETCH_TRENDING_MOVIE,
payload: TrendingMovieList,
};
}
export const getTrendingID = () => {
// @ts-ignore
return async dispatch => {
try {
const IDPromise = await fetch(config.base_url + "trending/movie/day?api_key=" + config.api_key);
const IDListJson = await IDPromise.json();
// @ts-ignore
const idList: string[] = IDListJson.results.map(elt => elt["id"]);
const MovieList: Movie[] = [];
Promise.all(idList.map(async elt => {
try {
return await fetch(config.base_url + "movie/" + elt + "?api_key=" + config.api_key + "&language=fr-FR");
} catch (err) {
console.log('Error', err);
}
})).then(function (responses) {
// @ts-ignore
Promise.all(responses.map(result => result.json()))
.then(function (elements) {
elements.forEach(elt => {
const infoJson = elt;
const genreRow: string[] = [];
// @ts-ignore
elt["genres"].map(genre => {
genreRow.push(genre.name);
});
console.log(new Movie(infoJson["id"], infoJson["title"], infoJson["poster_path"], infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"], genreRow, infoJson["overview"], infoJson["backdrop_path"]))
// @ts-ignore
MovieList.push(new Movie(infoJson["id"], infoJson["title"], infoJson["poster_path"], infoJson["runtime"], infoJson["vote_average"], infoJson["release_date"], genreRow, infoJson["overview"], infoJson["backdrop_path"]))
})
try {
dispatch(setinfoMovie(MovieList));
} catch (err) {
console.log('Error', err);
}
})
});
} catch (error) {
console.log('Error', error);
}
}
}
export const removeMovieTrending = (movie: Movie) => {
return {
type: POP_FIRST_TRENDING,
payload: movie
}
}
export const addMovieToWatchLater = (movie: Movie) => {
return {
type: ADD_WATCHLATER,
payload: movie
}
}
export const addMovieToFavourite = (movie: Movie) => {
return {
type: ADD_FAVOURITE,
payload: movie
}
}

@ -0,0 +1,12 @@
export const FETCH_TRENDING_ID : string = "FETCH_TRENDING_ID";
export const FETCH_TRENDING_MOVIE: string = "FETCH_TRENDING_MOVIE";
export const POP_FIRST_TRENDING: string = "POP_FIRST_TRENDING";
export const ADD_WATCHLATER : string = "ADD_WATCHLATER";
export const FETCH_WATCHLATER : string = "FETCH_WATCHLATER";
export const ADD_FAVOURITE : string = "ADD_FAVOURITE";
export const FETCH_FAVOURITE : string = "FETCH_FAVOURITE";
export const LOAD_WATCHLATER : string = "LOAD_WATCHLATER";
export const LOAD_FAVOURITE : string = "LOAD_FAVOURITE";

@ -0,0 +1,32 @@
import {POP_FIRST_TRENDING, FETCH_TRENDING_MOVIE, ADD_WATCHLATER, ADD_FAVOURITE, LOAD_WATCHLATER, LOAD_FAVOURITE} from "../constants";
import Movie from "../../model/Movie";
const initialState = {
trendingIDs: [],
trendingMovies: [] as Movie[],
watchLaterMovies: [] as Movie[],
favouriteMovies: [] as Movie[],
}
// @ts-ignore
export default (state = initialState, action) => {
switch (action.type) {
case LOAD_WATCHLATER:
// @ts-ignore
return {...state, watchLaterMovies: action.payload};
case LOAD_FAVOURITE:
// @ts-ignore
return {...state, favouriteMovies: action.payload};
case FETCH_TRENDING_MOVIE:
return {...state, trendingMovies: action.payload};
case POP_FIRST_TRENDING:
return {...state, trendingMovies: [...state.trendingMovies.filter((item: Movie) => item !== action.payload)]};
case ADD_WATCHLATER:
// @ts-ignore
return {...state, watchLaterMovies: [action.payload, ...state.watchLaterMovies]};
case ADD_FAVOURITE:
// @ts-ignore
return {...state, favouriteMovies: [action.payload, ...state.favouriteMovies]};
default:
return state;
}
}

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

@ -1,89 +1,148 @@
import {FlatList, StyleSheet, SafeAreaView, Text, View, Image, TextInput} from 'react-native'; import {FlatList, StyleSheet, View, TextInput, TouchableHighlight} from 'react-native';
import * as React from "react"; import * as React from "react";
import {BadgeFilm} from "./HomeScreen"; import {RootTabScreenProps} from "../types";
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome"; import {useSafeAreaInsets} from "react-native-safe-area-context";
import { faHeart} from "@fortawesome/free-solid-svg-icons"; import {useDispatch, useSelector} from 'react-redux';
import {RootTabScreenProps} from "../types.js"; import {useEffect, useState} from 'react';
import {getFavourite} from "../redux/actions/actions";
import Movie from "../model/Movie";
import {MovieListComponent} from "../components/MovieListComponent";
import MovieFinderScreenList from "./MovieFinderScreenList";
export default function FavoriteScreen({navigation}: RootTabScreenProps<'Favorite'>) {
export default function FavoriteScreen({ navigation }: RootTabScreenProps<'Favorite'>) { const [search, setSearch] = useState('');
return (
<SafeAreaView style={styles.container}>
<View style={{height: 50, justifyContent: "flex-start",flexDirection: 'row', paddingHorizontal:20, marginBottom: 15,marginVertical:5, alignItems:"flex-end"}} >
<FontAwesomeIcon icon={faHeart} style={{marginBottom: -5, marginRight: 20}} size={50} color="white" />
<Text style={{color: "white", fontSize:30}}>Favorite</Text>
</View>
<Image
source={require('../assets/images/delimiter.png')} style={{height: 2, width: 400, resizeMode: "stretch"}}
/>
<View style={{height:40, width:400, backgroundColor:"grey", borderRadius:20, marginVertical:10, alignSelf:"center"}}>
<TextInput style={{width:'100%', height:40, marginHorizontal:20}} ></TextInput>
</View>
<FlatList
data={[
{key: 'Devin'},
{key: 'Dan'},
{key: 'Dominic'},
{key: 'Jackson'},
{key: 'James'},
{key: 'Joel'},
{key: 'John'},
{key: 'Jillian'},
{key: 'Jimmy'},
{key: 'Julie'},
]}
renderItem={({item}) => <ListWidget name={item.key} ></ListWidget>}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 22,
backgroundColor: "#232323"
},
item: {
padding: 10,
fontSize: 18,
height: 44,
color: "white"
},
filmCard: {
width: 70,
height: 100,
borderRadius: 8,
},
});
type ListWidgetProps = {
name : String
} const [filteredDataSource, setFilteredDataSource] = useState<Movie[]>([]);
export function ListWidget(props: ListWidgetProps) { const [borderwidth, setBorderWidth] = useState(0);
return (
<View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'row', paddingHorizontal:20, marginVertical:5}} >
<Image
style={styles.filmCard}
source={{
uri: 'https://fr.web.img4.acsta.net/pictures/21/11/16/10/01/4860598.jpg',
}}
/>
<View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'column', paddingLeft:10}} >
<Text style={{color: "white", fontWeight:"bold", fontSize:25}}>{props.name}</Text>
<Text style={{color: "grey", fontWeight:"bold", fontSize:17}}>{props.name}</Text>
<View style={{marginVertical:10}}>
<BadgeFilm name={"Science-Ficton"}/>
</View>
</View>
</View>
const [masterDataSource] = useState([]);
); const insets = useSafeAreaInsets();
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: insets.top + 22,
backgroundColor: "#0E0E0E"
},
linearGradient: {
flex: 1,
paddingLeft: 15,
paddingRight: 15,
borderRadius: 5
},
item: {
padding: 10,
fontSize: 18,
height: 44,
color: "white"
},
filmCard: {
width: 70,
height: 100,
borderRadius: 8,
},
searchSection: {
height: 40,
width: 400,
backgroundColor: "#323232",
borderRadius: 20,
marginVertical: 10,
alignSelf: "center",
borderWidth: borderwidth,
borderColor: "rgba(255,255,255,0.22)"
},
searchBar: {
width: '100%',
height: 40,
marginHorizontal: 20,
color: "white"
},
titlePage: {
height: 50,
justifyContent: "flex-start",
flexDirection: 'row',
paddingHorizontal: 20,
marginBottom: 15,
marginVertical: 5,
alignItems: "flex-end"
},
icon: {
marginBottom: -5,
marginRight: 20
},
delimiter: {
height: 2,
width: 400,
resizeMode: "stretch"
},
h1: {
color: "white",
fontSize: 30
}
});
// @ts-ignore
const favouriteMovies = useSelector(state => state.appReducer.favouriteMovies);
const dispatch = useDispatch();
useEffect(() => {
const loadFavourite = async () => {
// @ts-ignore
dispatch(getFavourite());
};
loadFavourite();
}, [dispatch]);
const searchFilterFunction = (text: string) => {
if (text) {
const newData = favouriteMovies.filter(function (item: Movie) {
const itemData = item.original_title
? item.original_title.toUpperCase()
: ''.toUpperCase();
const textData = text.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setFilteredDataSource(newData);
setSearch(text);
} else {
setFilteredDataSource(masterDataSource);
setSearch(text);
}
};
const ToggleSearchBar = () => {
if (borderwidth === 0)
setBorderWidth(2)
else
setBorderWidth(0)
};
return (
// @ts-ignore
<MovieFinderScreenList page={"Favorite"}>
<View style={styles.searchSection}>
<TextInput style={styles.searchBar} onChangeText={(text) => searchFilterFunction(text)}
value={search}
placeholder="Rechercher ici..."
placeholderTextColor={"white"}
onFocus={ToggleSearchBar}
onBlur={ToggleSearchBar}
></TextInput>
</View>
<FlatList
data={search.length !== 0 ? filteredDataSource : favouriteMovies}
keyExtractor={item => item.original_title}
// @ts-ignore
renderItem={({item}) => <TouchableHighlight onPress={() => navigation.navigate("Info", {"item": item})}><MovieListComponent movie={item}></MovieListComponent></TouchableHighlight>}
/>
</MovieFinderScreenList>
);
} }

@ -1,37 +1,61 @@
import * as React from 'react'; import * as React from 'react';
import {Button,TouchableOpacity,ScrollView,View, Text, StyleSheet, Image, ImageBackground, SafeAreaView} from 'react-native'; import {TouchableOpacity, View, Text, StyleSheet, Image, ImageBackground, SafeAreaView} from 'react-native';
import {RootStackScreenProps} from "../types.js"; import {useEffect, useState} from "react";
import Rive from 'rive-react-native';
import {useRef} 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 {useDispatch, useSelector} from 'react-redux';
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 App({ navigation }: RootStackScreenProps<'Home'>) {
const riveRef = useRef(); export default function HomeScreen() {
const insets = useSafeAreaInsets(); // @ts-ignore
const trendingMovies = useSelector(state => state.appReducer.trendingMovies)
// @ts-ignore
const watchLaterMovies = useSelector(state => state.appReducer.watchLaterMovies)
// @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
const insets = useSafeAreaInsets()
const styles = StyleSheet.create({ const styles = StyleSheet.create({
background: { background1: {
backgroundColor: 'black', backgroundColor: 'black',
height: '100%', height: '100%',
width: '100%',
paddingTop: insets.top, paddingTop: insets.top,
}, },
background2: {
container:{ height: '100%',
width: '100%',
paddingTop: insets.top,
},
container: {
flex: 1, flex: 1,
}, },
filmCard: { filmCard: {
width: '80%', width: '85%',
height: '60%', justifyContent: 'center',
justifyContent:'center', marginLeft: 'auto',
marginLeft:'auto', marginRight: 'auto',
marginRight:'auto', borderRadius: 22,
borderRadius: 15, flex: 0.80,
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,
@ -39,10 +63,11 @@ export default function App({ navigation }: RootStackScreenProps<'Home'>) {
}, },
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,
@ -52,125 +77,276 @@ export default function App({ navigation }: RootStackScreenProps<'Home'>) {
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
}
})
useEffect(() => {
setInterval(() => {
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
const suggestedMovies = suggestedJson.map((element) => {
return element
})
console.log("suggested", suggestedMovies)
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()
}
}
}
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 popFirstTrending(props: Movie) {
dispatch(removeMovieTrending(props))
if (displayIndex == trendingMovies.length - 1) {
setdisplayIndex(0)
swiper.swipeLeft()
}
}
});
return ( return (
<SafeAreaView style={styles.background}>
<>
<ImageBackground blurRadius={20} <ImageBackground blurRadius={0}
style={{ style={styles.finishBackground}
position: 'absolute', source={require("../assets/images/background.png")
width: "120%", }
height: "120%",
justifyContent: "center",
alignItems: "center",
opacity: 0.28
}}
source={{
uri: 'https://fr.web.img4.acsta.net/pictures/21/11/16/10/01/4860598.jpg',
}}
></ImageBackground> ></ImageBackground>
<View style={styles.image}>
<Image {trendingMovies.length !== 0 && (
style={styles.filmCard}
source={{ <SafeAreaView style={styles.background1}>
uri: 'https://fr.web.img4.acsta.net/pictures/21/11/16/10/01/4860598.jpg',
}}
/> <ImageBackground blurRadius={29}
</View> style={styles.posterBackground}
<View style={{height:35, marginTop: 10, marginBottom: 15}}> source={{
<ScrollView uri: trendingMovies[displayIndex]?.poster_path,
horizontal={true} }}
showsHorizontalScrollIndicator={false}> ></ImageBackground>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> <HeaderMovie movie={trendingMovies[displayIndex]}></HeaderMovie>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre>
<BadgeGenre name={"cc"} isSelected={true}></BadgeGenre> <CardsSwipe
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> ref={(rf) => {
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> swiper = rf
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> }}
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> containerStyle={{zIndex: 20}}
<BadgeGenre name={"cc"} isSelected={false}></BadgeGenre> cards={trendingMovies}
loop={true}
onSwipedLeft={(index) => {
if (index < trendingMovies.length - 1) {
</ScrollView>
</View> setdisplayIndex(index + 1);
<View style={{ flexDirection: 'column', alignSelf: 'flex-start', alignItems: 'flex-start', paddingHorizontal: 30, flex: 1 }}>
} else
<View style={{ flexDirection: 'row', alignSelf: 'flex-start', justifyContent: 'flex-start', width: "100%"}}> setdisplayIndex(0)
<BadgeFilm name={"Science-fiction"}></BadgeFilm> }
<BadgeFilm name={"Science-fiction"}></BadgeFilm> }
<BadgeFilm name={"9:11"}></BadgeFilm> onSwipedRight={(index) => {
</View> if (index < trendingMovies.length)
<View> setdisplayIndex(index + 1)
<Text numberOfLines={1} style={{color: "white", fontSize: 28, fontWeight: "bold", paddingTop: 5}}>SPIDER-MAN No Way Home</Text> 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
style={styles.filmCard}
source={{
uri: card?.poster_path,
}}
/>
</>
)
)
}
/>
<View style={styles.buttonSection}>
<TouchableOpacity onPress={() => {
addWatchLater(trendingMovies[displayIndex]);
}}>
<Image
source={require('../assets/images/watchlater_button.png')} style={styles.button}
/>
</TouchableOpacity>
<TouchableOpacity onPress={
() => {
popFirstTrending(trendingMovies[displayIndex]);
}}>
<Image
source={require('../assets/images/delete_button.png')} style={styles.button}
/>
</TouchableOpacity>
<TouchableOpacity onPress={() => {
addFavourite(trendingMovies[displayIndex]);
}
}>
<Image
source={require('../assets/images/like_button.png')} style={styles.button}
/>
</TouchableOpacity>
</View> </View>
<Text style={{color: "grey", fontSize: 20, fontWeight: "bold"}}>Jean-Marc généreux</Text> <Timer hours={hours} minutes={minutes} seconds={seconds}></Timer>
</View>
<View style={{ flexDirection: 'row' ,alignItems: 'center', justifyContent: "space-evenly", paddingHorizontal: 30, height: '15%', width:'100%'}}>
<TouchableOpacity>
<Image
source={require('../assets/images/WatchLater.png')} style={{ resizeMode:"stretch", height:'65%', aspectRatio: 1,}}
/>
</TouchableOpacity>
<TouchableOpacity>
<Image
source={require('../assets/images/Generate.png')} style={{resizeMode:"stretch", height:'85%',aspectRatio: 1,}}
/>
</TouchableOpacity>
<TouchableOpacity>
<Image
source={require('../assets/images/Favorite.png')} style={{ resizeMode:"stretch", height:'65%', aspectRatio: 1,}}
/>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
type BadgeGenreProps = {
name : String
isSelected: Boolean
} </SafeAreaView>)}
{trendingMovies.length === 0 && (
<SafeAreaView style={styles.background2}>
<View style={styles.congratsSection}>
<Text style={styles.h1}>Félicitations !</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>
export function BadgeGenre(props: BadgeGenreProps) { <Timer2 hours={hours} minutes={minutes} seconds={seconds}></Timer2>
if(props.isSelected==false){ </View>
return (
<View style={{paddingHorizontal: 20, marginHorizontal: 5,height: 35, backgroundColor: '#2E2E2E', borderRadius: 20, justifyContent: "center"}} >
<Text style={{color: "white"}}>{props.name}</Text>
</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"}} >
<Text style={{color: "white", fontSize: 12, fontWeight:"bold"}}>{props.name}</Text>
</View>
);
}

@ -0,0 +1,477 @@
import * as React from 'react';
import {TouchableOpacity, ScrollView, View, Text, StyleSheet, Image, SafeAreaView, FlatList} from 'react-native';
import {RootStackScreenProps} from "../types";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import Movie from "../model/Movie";
import {LinearGradient} from 'expo-linear-gradient';
import {useEffect, useState} from "react";
import config from "../constants/config";
import YoutubeIframe from "react-native-youtube-iframe";
import Ionicons from "@expo/vector-icons/Ionicons";
import MinimalMovie from "../model/MinimalMovie";
import Review from "../model/review";
import Stars from "../components/StarsComponent";
import {formatTime} from "../model/formatTime";
export default function InfoScreen({navigation, route}: RootStackScreenProps<'Info'>) {
// @ts-ignore
const item: Movie = route.params.item
const insets = useSafeAreaInsets();
const [trailerPath, setTrailerPath] = useState("");
const [similarMovies, setSimilarMovies] = useState<MinimalMovie[]>([]);
const [review, setReview] = useState<Review[]>([]);
const [credit, setCredit] = useState<creditItem[]>();
const [paddingTopBackground, setPaddingTopBackground] = useState(0);
const [opacityBackground, setOpacityBackground] = useState(0.7);
const [scaleBackground, setScaleBackground] = useState(1);
const handleScroll = (event: any) => {
const {y} = event.nativeEvent.contentOffset;
let padTop = y / -20;
if (padTop <= 0)
setPaddingTopBackground(padTop);
setOpacityBackground(0.5 - y / 600);
let scale = 1 - y / -2000
if (scale >= 1)
setScaleBackground(scale);
};
type creditItem = [string, string, number];
type creditProps = {
data: creditItem[];
};
function CreditList({data}: creditProps) {
const renderItem = ({item}: { item: creditItem }) => (
<View style={styles.creditContainer}>
<View style={styles.bubble}>
<Image source={{uri: item[1]}} style={styles.photo}></Image>
<View style={styles.popularityDot}>
<Text style={styles.popularityLabel}>{item[2].toFixed(1).toString()}</Text>
<Ionicons name="md-star" size={13} color="#FFC42D"/>
</View>
</View>
<Text numberOfLines={2} style={styles.creditName}>{item[0]}</Text>
</View>
);
return (
<FlatList
style={{paddingBottom: 40}}
data={data}
horizontal={true}
showsHorizontalScrollIndicator={false}
renderItem={renderItem}
keyExtractor={(item) => item[0]}
/>
);
}
type SimilarMovieProps = {
movie: MinimalMovie;
};
function SimilarMovie(props: SimilarMovieProps) {
return (
<View style={styles.similarContainer}>
<Image source={{uri: props.movie.poster_path}} style={styles.similarPoster}></Image>
<Text numberOfLines={2} style={styles.similarTitleFilm}>{props.movie.original_title}</Text>
</View>
);
}
type ReviewProps = {
review: Review;
};
function ReviewComponent(props: ReviewProps) {
return (
<View style={styles.reviewContainer}>
<View style={styles.reviewInfo}>
<Image source={{uri: props.review.profile_path}} style={styles.imageProfile}></Image>
<View style={styles.infoContainer}>
<Text numberOfLines={1} style={styles.pseudo}>{props.review.pseudo}</Text>
<Text style={styles.date}>{props.review.date}</Text>
</View>
</View>
<Text numberOfLines={15} style={styles.message}>{props.review.message}</Text>
</View>
);
}
type InfoBadgeProps = {
texte: string
}
function InfoBadge(props: InfoBadgeProps) {
return (<View style={{paddingHorizontal: 15, paddingVertical: 7, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 10, justifyContent: "center", marginRight: 10}}>
<Text style={{color: "white", fontSize: 15}}>{props.texte}</Text>
</View>);
}
const styles = StyleSheet.create({
background1: {
height: '100%',
width: '100%',
paddingTop: insets.top,
},
body: {
backgroundColor: "#0E0E0E"
},
backgroundSection: {
height: "100%",
width: "100%",
position: "absolute"
},
back_drop: {
height: "45%",
top: paddingTopBackground,
width: '100%',
opacity: opacityBackground,
position: "absolute",
transform: [{scale: scaleBackground}],
},
gradientFade: {
height: "30%",
top: "25%"
},
backButton: {
position: "absolute",
top: 10,
left: 5
},
list: {
height: "100%"
},
section1: {
paddingHorizontal: 35
},
title: {
color: "white",
fontSize: 43,
fontWeight: "bold",
paddingBottom: 10,
paddingTop: "45%"
},
characteristics: {
flexDirection: "row",
width: "100%",
justifyContent: "flex-start"
},
stars: {
flexDirection: "row",
width: "100%",
justifyContent: "flex-start",
alignItems: "center",
paddingBottom: 30
},
starsLabel: {
color: "#FFC42D",
fontWeight: "bold",
paddingLeft: 10,
fontSize: 16
},
player: {
borderRadius: 10,
overflow: "hidden"
},
resume: {
color: "#B3B3B3",
paddingTop: 30,
fontSize: 17
},
creditSection: {
paddingTop: 30
},
creditTitle: {
color: "#2998FD",
paddingBottom: 20,
paddingLeft: 35,
fontSize: 17,
fontWeight: "600"
},
similarSection: {
paddingTop: 30
},
similarTitle: {
color: "#2998FD",
paddingLeft: 35,
fontSize: 17,
fontWeight: "600",
paddingBottom: 20
},
similarContainer: {
width: 90,
marginHorizontal: 7
},
similarPoster: {
height: 130,
width: 90,
borderRadius: 8
},
similarTitleFilm: {
color: "#DADADA",
paddingTop: 5,
fontWeight: "300"
},
reviewSection: {
paddingTop: 30
},
reviewTitle: {
color: "#2998FD",
paddingLeft: 35,
fontSize: 17,
fontWeight: "600",
paddingBottom: 10
},
reviewContainer: {
marginHorizontal: 7,
width: 300,
padding: 20,
backgroundColor: "#09090F",
marginVertical: 10,
borderRadius: 14,
borderWidth: 0.8,
borderColor: "rgba(223,223,223,0.14)"
},
reviewInfo: {
flexDirection: "row",
paddingBottom: 20
},
imageProfile: {
height: 50,
width: 50,
borderRadius: 100
},
infoContainer: {
paddingLeft: 10,
flexDirection: "row",
justifyContent: "space-between",
width: "80%",
alignItems: "center"
},
pseudo: {
color: "white",
paddingTop: 5,
fontWeight: "700",
fontSize: 16
},
date: {
color: "grey",
paddingTop: 5,
fontWeight: "500",
fontSize: 14
},
message: {
color: "#B3B3B3",
paddingTop: 5,
fontWeight: "400"
},
creditContainer: {
width: 90,
marginHorizontal: 7,
alignItems: "center"
},
bubble: {
justifyContent: "center"
},
photo: {
height: 90,
width: 90,
borderRadius: 200,
borderWidth: 3,
borderColor: "rgba(255,255,255,0.8)"
},
popularityDot: {
backgroundColor: "white",
borderRadius: 20,
padding: 2,
paddingHorizontal: 5,
justifyContent: "center",
alignItems: "center",
position: "absolute",
bottom: 0,
right: 0,
flexDirection: "row"
},
popularityLabel: {
color: "black",
fontWeight: "500",
paddingRight: 4
},
creditName: {
color: "#DADADA",
paddingTop: 5,
fontWeight: "300"
}
});
const getTriller = async () => {
const trailerResponse = (await fetch(config.base_url + "movie/" + item.id + "/videos?api_key=" + config.api_key + "&language=fr-FR"));
const trailerJson = await trailerResponse.json();
//console.log("trailer", trailerJson)
// @ts-ignore
const trailer_key = trailerJson.results.slice(0, 1).map((elt) => {
if (elt["type"] === "Trailer" && elt["site"] === "YouTube") {
return elt["key"];
}
});
//console.log("key", trailer_key)
setTrailerPath(trailer_key);
}
const getCredits = async () => {
const creditResponse = (await fetch(config.base_url + "movie/" + item.id + "/credits?api_key=" + config.api_key + "&language=fr-FR"));
const creditJson = await creditResponse.json();
// @ts-ignore
let creditList = creditJson.cast.map((elt) => {
if (elt["popularity"])
return [elt["name"], 'https://image.tmdb.org/t/p/w185' + elt["profile_path"], elt["popularity"]]
});
creditList = creditList.slice(0, 5).sort((a: [fullname: string, profile_path: string, popularity: number], b: [fullname: string, profil_path: string, popularity: number]) => b[2] - a[2]);
setCredit(creditList);
}
const getSimilarMovies = async () => {
const SimilarMoviesResponse = (await fetch(config.base_url + "movie/" + item.id + "/recommendations?api_key=" + config.api_key + "&language=fr-FR"));
const SimilarMoviesJson = await SimilarMoviesResponse.json();
// @ts-ignore
const SimilarMoviesList = SimilarMoviesJson.results.slice(0, 10).map((elt) => {
return new MinimalMovie(elt["original_title"], elt["poster_path"])
});
setSimilarMovies(SimilarMoviesList);
}
const getReview = async () => {
const ReviewResponse = (await fetch(config.base_url + "movie/" + item.id + "/reviews?api_key=" + config.api_key + "&language=us-EN&page=1"));
const ReviewJson = await ReviewResponse.json();
// @ts-ignore
let ReviewList = ReviewJson.results.slice(0, 5).map((elt) => {
// @ts-ignore
const newreview = new Review(elt["content"], elt["author_details"].avatar_path, elt["created_at"], elt["author"])
return newreview
});
ReviewList = ReviewList.filter((review: Review, index: number, array: Review[]) => {
return array.findIndex((item: Review) => item.pseudo === review.pseudo) === index;
});
setReview(ReviewList);
}
useEffect(() => {
getTriller();
getSimilarMovies();
getCredits();
getReview();
}, []);
return (
<View style={styles.body}>
<View style={styles.backgroundSection}>
<Image
style={styles.back_drop}
source={{
uri: item.backdrop_path,
}}
></Image>
<LinearGradient
// Background Linear Gradient
colors={['rgba(0,0,0,0.8)', 'transparent']}
/>
<LinearGradient style={styles.gradientFade}
// Button Linear Gradient
colors={['rgba(14,14,14,0)', 'rgba(14,14,14,0.7)', 'rgba(14,14,14,1)', 'rgba(14,14,14,1)']}>
</LinearGradient>
</View>
<SafeAreaView style={styles.background1}>
<TouchableOpacity onPress={() => navigation.goBack()} style={{zIndex: 100}}>
<Ionicons name="ios-arrow-back" size={30} color="white" style={styles.backButton}/>
</TouchableOpacity>
<ScrollView style={styles.list} showsVerticalScrollIndicator={false} onScroll={handleScroll} scrollEventThrottle={4}
>
<View style={styles.section1}>
<Text style={styles.title} numberOfLines={2}>{item.original_title}</Text>
<View style={styles.characteristics}>
<InfoBadge texte={`${item.genres[0]} ${item.genres[1] !== undefined ? ", " + item.genres[1] : ""}`}></InfoBadge>
<InfoBadge texte={item.release_date}></InfoBadge>
<InfoBadge texte={formatTime(item.runtime)}></InfoBadge>
</View>
<View style={styles.stars}>
<Stars note={item.vote_average} size={120}></Stars>
<Text style={styles.starsLabel}>{item.vote_average.toFixed(1)}</Text>
</View>
{trailerPath !== "" && (<YoutubeIframe webViewStyle={styles.player} height={195} play={false} videoId={trailerPath}/>)}
<Text style={styles.resume}>{item.overview}</Text>
</View>
{credit !== undefined && (
<View style={styles.creditSection}>
<Text style={styles.creditTitle}>Crédits</Text>
<CreditList data={credit}></CreditList>
</View>
)}
{similarMovies.length !== 0 && (
<View style={styles.similarSection}>
<Text style={styles.similarTitle}>Recommendations</Text>
<FlatList
showsHorizontalScrollIndicator={false}
data={similarMovies}
horizontal={true}
keyExtractor={item => item.original_title}
renderItem={({item}) =>
<SimilarMovie movie={item}></SimilarMovie>
}
/></View>
)}
{review.length !== 0 && (
<View style={styles.reviewSection}>
<Text style={styles.reviewTitle}>Commentaires</Text>
<FlatList
showsHorizontalScrollIndicator={false}
data={review}
horizontal={true}
keyExtractor={item => item.pseudo}
renderItem={({item}) =>
<ReviewComponent review={item}></ReviewComponent>
}
/></View>
)}
</ScrollView>
</SafeAreaView>
</View>
)
}

@ -0,0 +1,57 @@
import {Image, SafeAreaView, StyleSheet, Text, View} from "react-native";
import {FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
import {faClock, faHeart} from "@fortawesome/free-solid-svg-icons";
import * as React from "react";
import {useSafeAreaInsets} from "react-native-safe-area-context";
type Props = {
page: string;
children : React.PropsWithChildren<{}>
};
export default function MovieFinderScreenList(props : Props){
const insets = useSafeAreaInsets();
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: insets.top + 22,
backgroundColor: "#0E0E0E"
},
titlePage: {
height: 50,
justifyContent: "flex-start",
flexDirection: 'row',
paddingHorizontal: 20,
marginBottom: 15,
marginVertical: 5,
alignItems: "flex-end"
},
icon: {
marginBottom: -5,
marginRight: 20
},
delimiter: {
height: 2,
width: 400,
resizeMode: "stretch"
},
h1: {
color: "white",
fontSize: 30
}
})
return (
// @ts-ignore
<SafeAreaView style={styles.container}>
<View style={styles.titlePage}>
<FontAwesomeIcon icon={props.page=="Favorite"?faHeart:faClock} style={styles.icon} size={40} color="white"/>
{props.page=="Favorite"?<Text style={styles.h1}>Favourite</Text>:<Text style={styles.h1}>Watch Later</Text>}
</View>
<Image source={require('../assets/images/delimiter.png')} style={styles.delimiter}/>
{props.children}
</SafeAreaView>
);
};

@ -1,19 +1,51 @@
import {FlatList, StyleSheet, SafeAreaView, Text, View, Image, TextInput} from 'react-native'; import {FlatList, StyleSheet, View, TextInput, TouchableHighlight} from 'react-native';
import * as React from "react"; import * as React from "react";
import {BadgeFilm} from "./HomeScreen"; import {RootTabScreenProps} from "../types";
import { FontAwesomeIcon} from "@fortawesome/react-native-fontawesome";
import { faClock} from "@fortawesome/free-solid-svg-icons";
import LinearGradient from 'react-native-linear-gradient';
import {RootTabScreenProps} from "../types.js";
import {useSafeAreaInsets} from "react-native-safe-area-context"; import {useSafeAreaInsets} from "react-native-safe-area-context";
export default function WatchLaterScreen({ navigation }: RootTabScreenProps<'WatchLater'>) { import {useDispatch, useSelector} from 'react-redux';
import {useEffect, useState} from 'react';
import {getWatchLater} from "../redux/actions/actions";
import Movie from "../model/Movie";
import {MovieListComponent} from "../components/MovieListComponent";
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: 22, paddingTop: insets.top + 22,
backgroundColor: "#232323" backgroundColor: "#0E0E0E"
}, },
linearGradient: { linearGradient: {
flex: 1, flex: 1,
@ -31,81 +63,84 @@ export default function WatchLaterScreen({ navigation }: RootTabScreenProps<'Wat
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
}
}); });
return (
<SafeAreaView style={styles.container}>
<View style={{height: 50, justifyContent: "flex-start",flexDirection: 'row', paddingHorizontal:20, marginBottom: 15,marginVertical:5, alignItems:"flex-end"}} >
<FontAwesomeIcon icon={faClock} style={{marginBottom: -5, marginRight: 20}} size={50} color="white" />
<Text style={{color: "white", fontSize:30}}>Watch Later</Text> useEffect(() => {
</View> const loadWatchLater = async () => {
<Image // @ts-ignore
source={require('../assets/images/delimiter.png')} style={{height: 2, width: 400, resizeMode:"stretch"}} dispatch(getWatchLater());
/> };
<View style={{height:40, width:400, backgroundColor:"grey", borderRadius:20, marginVertical:10, alignSelf:"center"}}> loadWatchLater();
<TextInput style={{width:'100%', height:40, marginHorizontal:20}} ></TextInput> }, [dispatch]);
</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 ToggleSearchBar = () => {
if (borderwidth === 0)
setBorderWidth(2)
else
setBorderWidth(0)
};
type ListWidgetProps = {
name : String
}
export function ListWidget(props: ListWidgetProps) {
const insets = useSafeAreaInsets();
const styles = StyleSheet.create({
filmCard: {
width: 70,
height: 100,
borderRadius: 8,
return (
// @ts-ignore
<MovieFinderScreenList page={"Watch Later"}>
}, <View style={styles.searchSection}>
}); <TextInput style={styles.searchBar} onChangeText={(text) => searchFilterFunction(text)}
return ( value={search}
<View style={{height: 100, borderRadius: 20, justifyContent: "flex-start", flexDirection: 'row', paddingHorizontal:20, marginVertical:5}} > placeholder="Rechercher ici..."
<Image placeholderTextColor={"white"}
style={styles.filmCard} onFocus={ToggleSearchBar}
source={{ onBlur={ToggleSearchBar}
uri: 'https://fr.web.img4.acsta.net/pictures/21/11/16/10/01/4860598.jpg', ></TextInput>
}}
/>
<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> </View>
<FlatList
data={search.length !== 0 ? filteredDataSource : watchLaterMovies}
); keyExtractor={item => item.original_title}
// @ts-ignore
renderItem={({item}) => <TouchableHighlight onPress={() => navigation.navigate("Info", {"item": item})}><MovieListComponent movie={item}></MovieListComponent></TouchableHighlight>}
/>
</MovieFinderScreenList>
);
} }

@ -0,0 +1,68 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import Movie from "../model/Movie"
export const getFavouriteList = async () => {
try {
const value = await AsyncStorage.getItem('favourite');
if (value === null) {
return []
}
const favouriteList: Movie[] = await JSON.parse(value)
return favouriteList
} catch (error) {
console.log(error)
return null
}
}
export const setFavouriteList = async (favouriteList: Movie[]) => {
try {
await AsyncStorage.setItem('favourite', JSON.stringify(favouriteList))
} catch (error) {
console.log(error)
}
}
export const getWatchLaterList = async () => {
try {
const value = await AsyncStorage.getItem('watchLater')
if (value === null) {
return []
}
const watchLaterList: Movie[] = await JSON.parse(value)
return watchLaterList
} catch (error) {
console.log(error)
return null
}
};
export const setWatchLaterList = async (watchLaterList: Movie[]) => {
try {
await AsyncStorage.setItem('watchLater', JSON.stringify(watchLaterList))
} catch (error) {
console.log(error)
}
}
export const getMovieList = async () => {
try {
const value = await AsyncStorage.getItem('movie')
if (value === null) {
return []
}
const movieList: Movie[] = await JSON.parse(value)
return movieList
} catch (error) {
console.log(error)
return null
}
}
export const setMovieList = async (movieList: Movie[]) => {
try {
await AsyncStorage.setItem('movie', JSON.stringify(movieList))
} catch (error) {
console.log(error)
}
}

@ -3,39 +3,44 @@
* 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 {
} }
}
} }
export type RootStackParamList = { export type RootStackParamList = {
Root: NavigatorScreenParams<RootTabParamList> | undefined; Root: NavigatorScreenParams<RootTabParamList> | undefined;
Modal: undefined; Modal: undefined;
NotFound: undefined; NotFound: undefined;
Home: undefined; Home: undefined;
WatchLater: undefined; WatchLater: undefined;
Favorite: undefined;
Info: undefined;
Favorite: undefined;
}; };
export type RootStackScreenProps<Screen extends keyof RootStackParamList> = NativeStackScreenProps< export type RootStackScreenProps<Screen extends keyof RootStackParamList> = NativeStackScreenProps<
RootStackParamList, RootStackParamList,
Screen Screen
>; >;
export type RootTabParamList = { 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<
BottomTabScreenProps<RootTabParamList, Screen>, BottomTabScreenProps<RootTabParamList, Screen>,
NativeStackScreenProps<RootStackParamList> NativeStackScreenProps<RootStackParamList>
>; >;

Loading…
Cancel
Save