Commit ec63ee8a authored by Peter Skaar Nordby's avatar Peter Skaar Nordby
Browse files

Merge branch '15-loading-spinner-movielist' into 'master'

Resolve "Loading Spinner Movielist"

Closes #15

See merge request !13
parents 1afca964 de603976
......@@ -59,7 +59,7 @@ const FilterPane: React.FC<FilterPaneProps> = ({
const styles = StyleSheet.create({
container: {
marginTop: 96,
marginTop: 8,
marginHorizontal: 16,
width: '95%',
display: 'flex',
......
......@@ -5,6 +5,7 @@ import { signIn, signUp } from '../store/ducks/auth/actions';
import { ApplicationState } from '../store/interface';
import Logo from '../assets/images/cinema.png';
import {
Alert,
View,
Text,
Image,
......@@ -12,8 +13,10 @@ import {
useWindowDimensions,
Button,
Pressable,
ActivityIndicator,
} from 'react-native';
import CustomInput from './CustomInput';
import AwesomeAlert from 'react-native-awesome-alerts';
export default function App() {
const { height } = useWindowDimensions();
......@@ -43,6 +46,7 @@ export default function App() {
const [isLogin, setIsLogin] = useState(true);
const dispatch = useDispatch();
const { loading, error } = useSelector(({ auth }: ApplicationState) => auth);
const [alertMessage, setAlertmessage] = useState('');
// Form completed
const onFinish = (values: FormValues) => {
......@@ -62,17 +66,27 @@ export default function App() {
// TODO If the redux state contains an error, display an error alert
useEffect(() => {
if (error) {
console.log('Show error');
setAlertmessage(error);
} else {
setAlertmessage('');
}
}, [error]);
const styles = StyleSheet.create({
alert: {
width: 400,
},
alertTitle: {
color: 'red',
},
root: {
alignItems: 'center',
padding: 20,
flex: 1,
justifyContent: 'center',
width: '70%',
height: '100%',
},
logo: {
width: '70%',
minWidth: 200,
maxWidth: 300,
maxHeight: 200,
},
......@@ -92,7 +106,7 @@ export default function App() {
});
return (
<View>
<View style={styles.root}>
<Image
source={Logo}
style={[styles.logo, { height: height * 0.3 }]}
......@@ -144,6 +158,20 @@ export default function App() {
</Pressable>
</View>
</FormProvider>
<AwesomeAlert
contentContainerStyle={styles.alert}
titleStyle={styles.alertTitle}
show={alertMessage.length > 0}
title={`${isLogin ? 'Login' : 'Registration'} failed`}
message={`Server responded with: ${alertMessage}`}
closeOnTouchOutside={true}
showConfirmButton={true}
confirmText="Ok"
onConfirmPressed={() => {
setAlertmessage('');
}}
/>
{loading && <ActivityIndicator size="large" color="#00ff00" />}
</View>
);
}
import { StatusBar } from 'expo-status-bar';
import * as React from 'react';
import { Platform, StyleSheet, Image, useWindowDimensions, Button, ScrollView } from 'react-native';
import {
Button,
Image,
ScrollView,
StyleSheet,
useWindowDimensions,
} from 'react-native';
import Lord from '../assets/images/lord.jpg';
import { MovieDetail } from '../models/movieDetail.model';
import ProfileInfo from './ProfileInfo';
import { Text, View } from './Themed';
import Lord from "../assets/images/lord.jpg";
import { movies } from '../store/ducks/movies';
import KeyStatisticsItem from './KeyStatisticsItem';
import Seperator from './Seperator';
import Pill from './Pill';
import WrapperStatistic from 'antd/lib/statistic/Statistic';
import Seperator from './Seperator';
import { Text, View } from './Themed';
interface MovieDetailProps {
movieDetail: MovieDetail;
}
export default function MovieDetailScreen({movieDetail}: MovieDetailProps) {
const {height} = useWindowDimensions();
export default function MovieDetailScreen({ movieDetail }: MovieDetailProps) {
const { height } = useWindowDimensions();
const styles = StyleSheet.create({
root: {
......@@ -28,10 +30,10 @@ export default function MovieDetailScreen({movieDetail}: MovieDetailProps) {
height: 350,
},
container: {
width: "100%",
width: '100%',
padding: 15,
marginVertical: 5,
alignItems: "center",
alignItems: 'center',
borderRadius: 5,
},
subText: {
......@@ -45,98 +47,101 @@ export default function MovieDetailScreen({movieDetail}: MovieDetailProps) {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start'
alignItems: 'flex-start',
},
genres: {
flex: 1,
flexWrap: "wrap",
flexDirection: "row",
flex: 1,
flexWrap: 'wrap',
flexDirection: 'row',
marginTop: 15,
paddingBottom: "1rem",
}
})
paddingBottom: '1rem',
},
});
return (
<ScrollView style={styles.root}>
<View style={{alignItems: "center"}}>
<Image source={Lord} style={[styles.logo, {height: height*0.5}]} resizeMode="contain"/>
</View>
<View style={{alignItems: "center"}}>
Stars -{'>'} To be implemented
</View>
<View>
<Button onPress={() => console.log("To be implemented")} title={"Give a rating"}/>
</View>
<View style={styles.key_number_container}>
<KeyStatisticsItem title="RUNTIME" statistics={String(movieDetail.runtime)}/>
<KeyStatisticsItem title="IMDG-RATING" statistics={String(movieDetail.avgRating)}/>
<KeyStatisticsItem title="RATING" statistics={String(movieDetail.avgRating)}/>
<KeyStatisticsItem title="PGA-RATING" statistics={String(movieDetail.avgRating)}/>
</View>
<Seperator/>
<View>
<Text style={{fontSize: 22}}>
{movieDetail.year}
</Text>
<Text style={{fontSize: 18}}>
{movieDetail.title}
</Text>
<View style={styles.genres}>
{movieDetail.genres.map((genre) => <Pill text={genre}>genre</Pill>)}
</View>
</View>
<Seperator/>
<Text>
{movieDetail.plot}
</Text>
<Seperator/>
{LabelAndText("DIRECTORS", "Joshua King")}
<Seperator/>
{LabelAndText("PRODUCTION", "Paramount Pictures, W365")}
<Seperator/>
{LabelAndText("WRITERS", "Phil Hay, Matt Manfredi, Peter Chung")}
<Seperator/>
{LabelAndText("STARRING ACTORS", "Charlize Theron, Frances McDormand, Sophie Okonedo")}
<Seperator/>
<View style={{ alignItems: 'center' }}>
<Image
source={Lord}
style={[styles.logo, { height: height * 0.5 }]}
resizeMode="contain"
/>
</View>
<View style={{ alignItems: 'center' }}>
Stars -{'>'} To be implemented
</View>
<View>
<Button
onPress={() => console.log('To be implemented')}
title={'Give a rating'}
/>
</View>
<View style={styles.key_number_container}>
<KeyStatisticsItem
title="RUNTIME"
statistics={String(movieDetail.runtime)}
/>
<KeyStatisticsItem
title="IMDG-RATING"
statistics={String(movieDetail.avgRating)}
/>
<KeyStatisticsItem
title="RATING"
statistics={String(movieDetail.avgRating)}
/>
<KeyStatisticsItem
title="PGA-RATING"
statistics={String(movieDetail.avgRating)}
/>
</View>
<Seperator />
<View>
<Text style={{ fontSize: 22 }}>{movieDetail.year}</Text>
<Text style={{ fontSize: 18 }}>{movieDetail.title}</Text>
<View style={styles.genres}>
{movieDetail.genres.map((genre) => (
<Pill text={genre}>genre</Pill>
))}
</View>
</View>
<Seperator />
<Text>{movieDetail.plot}</Text>
<Seperator />
{LabelAndText('DIRECTORS', 'Joshua King')}
<Seperator />
{LabelAndText('PRODUCTION', 'Paramount Pictures, W365')}
<Seperator />
{LabelAndText('WRITERS', 'Phil Hay, Matt Manfredi, Peter Chung')}
<Seperator />
{LabelAndText(
'STARRING ACTORS',
'Charlize Theron, Frances McDormand, Sophie Okonedo'
)}
<Seperator />
<View>
Reviews -{'>'} To be implemented
</View>
<View>Reviews -{'>'} To be implemented</View>
</ScrollView>
);
}
function LabelAndText(label: string, text: string){
const styles= StyleSheet.create({
function LabelAndText(label: string, text: string) {
const styles = StyleSheet.create({
label: {
textTransform: "capitalize",
fontWeight: "bold",
textTransform: 'capitalize',
fontWeight: 'bold',
marginRight: 5,
},
container: {
flex: 1,
flexWrap: "wrap",
flexDirection: "row",
}
})
flexWrap: 'wrap',
flexDirection: 'row',
},
});
return(
return (
<View style={styles.container}>
<Text style={styles.label}>
{label}:
</Text>
<Text>
{text}
</Text>
<Text style={styles.label}>{label}:</Text>
<Text>{text}</Text>
</View>
)
}
\ No newline at end of file
);
}
import React, { useEffect, useState } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useSelector } from "react-redux";
import { FetchMovieParams, MovieEntity } from "../store/ducks/movies/types";
import { ApplicationState } from "../store/interface";
import MovieCard from "./MovieCard";
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
FlatList,
StyleSheet,
Text,
View,
} from 'react-native';
import { useSelector } from 'react-redux';
import { FetchMovieParams, MovieEntity } from '../store/ducks/movies/types';
import { ApplicationState } from '../store/interface';
import MovieCard from './MovieCard';
export type MovieTableProps = {
path: string;
query: FetchMovieParams;
movies: MovieEntity[];
currentPage: number;
moviesLoading: boolean;
onPageScroll: () => void;
};
......@@ -18,59 +24,95 @@ const MovieTable: React.FC<MovieTableProps> = ({
path,
query,
movies,
moviesLoading,
currentPage,
onPageScroll,
}: MovieTableProps) => {
const { data, documentCount, loading, error } = useSelector(
const { documentCount } = useSelector(
({ movies }: ApplicationState) => movies
);
const [isFull, setIsfull] = useState(false);
useEffect(() => {
setIsfull(!(movies.length < documentCount));
}, [movies]);
return (
<View style={styles.movieList}>
<FlatList
style={{ width: "100%", height: "100%" }}
columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }}
contentContainerStyle={styles.movieItems}
data={movies}
numColumns={2}
keyExtractor={(movie) => movie.id}
onEndReached={({ distanceFromEnd }) => {
// Prevent bug where onEndReached is called multiple times
if (distanceFromEnd < 0) return;
// Prevent fetching if all movies are loaded
if (currentPage >= Math.ceil(documentCount / query.perPage)) return;
// Dispatch scroll event to parent component
onPageScroll();
}}
onEndReachedThreshold={1.5}
initialNumToRender={query.perPage}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => <MovieCard movie={item} />}
ListFooterComponent={() => (
<>{loading && <Text style={styles.loading}>Loading...</Text>}</>
)}
/>
{movies.length > 0 || moviesLoading ? (
<FlatList
style={styles.movieFlatList}
columnWrapperStyle={{ flex: 1, justifyContent: 'space-around' }}
contentContainerStyle={styles.movieItems}
data={movies}
numColumns={2}
keyExtractor={(movie) => movie.id}
onEndReached={({ distanceFromEnd }) => {
// Prevent bug where onEndReached is called multiple times
if (distanceFromEnd < 0) return;
// Prevent fetching if all movies are loaded
if (currentPage >= Math.ceil(documentCount / query.perPage)) {
setIsfull(true);
return;
}
setIsfull(false);
// Dispatch scroll event to parent component
onPageScroll();
}}
onEndReachedThreshold={1.5}
initialNumToRender={query.perPage}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => <MovieCard movie={item} />}
ListFooterComponent={() => (
<>
{!isFull ? (
<>
<Text style={styles.loading}>Loading...</Text>
<ActivityIndicator size="large" color="#00ff00" />
</>
) : (
<>
{movies.length > 4 && (
<Text style={styles.loading}>--- No more movies ---</Text>
)}
</>
)}
</>
)}
/>
) : (
<Text style={styles.loading}>No Movies Found</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
movieItems: {
justifyContent: "center",
justifyContent: 'center',
flexGrow: 1 / 2,
backgroundColor: "white",
backgroundColor: 'white',
},
movieFlatList: {
width: '100%',
height: '100%',
marginBottom: 4,
},
movieList: {
marginTop: 4,
width: "95%",
height: "95%",
display: "flex",
alignItems: "center",
textAlign: "center",
width: '95%',
height: '95%',
alignItems: 'center',
textAlign: 'center',
flex: 1,
},
loading: {
fontWeight: "bold",
marginBottom: 8,
flex: 1,
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
textAlign: 'center',
},
});
......
import React, { useState, useEffect, useCallback } from "react";
import { StyleSheet } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import FilterPane, { FilterPaneProps } from "../components/FilterPane";
import React, { useState, useEffect, useCallback } from 'react';
import { StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import FilterPane, { FilterPaneProps } from '../components/FilterPane';
import MovieTable, { MovieTableProps } from "../components/MovieTable";
import { View } from "../components/Themed";
import MovieTable, { MovieTableProps } from '../components/MovieTable';
import { View } from '../components/Themed';
import {
FilterKeys,
FilterValues,
SortDirection,
SortKeys,
} from "../constants/filterOptions/interface";
import { fetchMovies } from "../store/ducks/movies/actions";
} from '../constants/filterOptions/interface';
import { fetchMovies } from '../store/ducks/movies/actions';
import {
FetchMovieParams,
initialQuery,
MovieEntity,
} from "../store/ducks/movies/types";
import { ApplicationState } from "../store/interface";
import { RootTabScreenProps } from "../types";
} from '../store/ducks/movies/types';
import { ApplicationState } from '../store/interface';
import { RootTabScreenProps } from '../types';
export default function MovieTableScreen({
navigation,
}: RootTabScreenProps<"Movies">) {
}: RootTabScreenProps<'Movies'>) {
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState<number>(1);
const [movies, setMovies] = useState<MovieEntity[]>([]);
const [query, setQuery] = useState<FetchMovieParams>(initialQuery);
const [moviesLoading, setMoviesLoading] = useState<boolean>(true);
const { data, loading, error } = useSelector(
({ movies }: ApplicationState) => movies
......@@ -50,6 +51,7 @@ export default function MovieTableScreen({
});
setMovies([...moviesToAdd]);
}
setMoviesLoading(false);
// Reset loaded content when component unmounts
return () => {
setMovies([]);
......@@ -60,6 +62,7 @@ export default function MovieTableScreen({
// Update the query state with new values
// Reset pagination increments and loaded movies, if reset is true
function updateQuery(newQuery: FetchMovieParams, reset = true) {
setMoviesLoading(true);
let { page, ...other } = newQuery;
if (reset) {
setMovies([]);
......@@ -117,10 +120,11 @@ export default function MovieTableScreen({
// Map properties to the movie table component
const mapStateToMovieTableProps: MovieTableProps = {
path: "/screens/TabOneScreen.tsx",
path: '/screens/TabOneScreen.tsx',
query,
movies,
currentPage,
moviesLoading,
onPageScroll: useCallback(() => handlePageScroll(), [query]),
};
......@@ -135,19 +139,18 @@ export default function MovieTableScreen({
const styles = StyleSheet.create({
container: {
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
title: {
fontSize: 20,
fontWeight: "bold",
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: "90%",
width: '90%',
},
});
......@@ -6843,6 +6843,13 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-native-awesome-alerts@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/react-native-awesome-alerts/-/react-native-awesome-alerts-1.5.2.tgz#eb8e05a8bc22ac304c59f82eab6be19308942c32"
integrity sha512-PPTzKLpwDKbjeghvrRkg7OunND4C7d4bQORVLBnhqUK2z2PfZiI3UCULU0tczzeOyavv5hVSrYNXKPXlvhfmVg==
dependencies:
prop-types "^15.7.2"
react-native-codegen@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.6.tgz#b3173faa879cf71bfade8d030f9c4698388f6909"