Commit f21889ad authored by ErlendHer's avatar ErlendHer
Browse files

Merge branch '2-implementere-filtrering-av-filmer'

parents e1d651f2 602b5aeb
......@@ -9,6 +9,7 @@ npm-debug.*
*.mobileprovision
*.orig.*
web-build/
.log
# macOS
.DS_Store
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider } from 'react-redux';
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import AppLoading from "expo-app-loading";
import { StatusBar } from "expo-status-bar";
import React, { useEffect, useState } from "react";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { Provider } from "react-redux";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import useCachedResources from './hooks/useCachedResources';
import useColorScheme from './hooks/useColorScheme';
import Navigation from './navigation';
import configureStore from './store';
import useCachedResources from "./hooks/useCachedResources";
import useColorScheme from "./hooks/useColorScheme";
import Navigation from "./navigation";
import configureStore from "./store";
// Setup Redux store
// eslint-disable-next-line @typescript-eslint/no-explicit-any
......@@ -20,7 +21,7 @@ export default function App() {
const colorScheme = useColorScheme();
if (!isLoadingComplete) {
return null;
return <AppLoading />;
} else {
return (
<SafeAreaProvider>
......
import React from "react";
import { Controller, useFormContext } from "react-hook-form";
import {View, Text, TextInput, StyleSheet, useWindowDimensions} from "react-native";
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import {
View,
Text,
TextInput,
StyleSheet,
useWindowDimensions,
} from 'react-native';
interface CustomInputProps{
error: boolean,
name: string,
placeholder: string;
secureTextEntry?: boolean;
errorMessage: string;
required: boolean;
interface CustomInputProps {
error: boolean;
name: string;
placeholder: string;
secureTextEntry?: boolean;
errorMessage: string;
required: boolean;
}
export default function CustomInput({required, errorMessage, error, name, placeholder, secureTextEntry}: CustomInputProps) {
export default function CustomInput({
required,
errorMessage,
error,
name,
placeholder,
secureTextEntry,
}: CustomInputProps) {
const { control } = useFormContext();
const { height } = useWindowDimensions();
const {control} = useFormContext();
const {height} = useWindowDimensions();
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
width: '100%',
minHeight: 30,
const styles = StyleSheet.create({
container: {
backgroundColor: "white",
width: "100%",
minHeight: 30,
borderColor: "#e8e8e8",
borderWidth: 1,
borderRadius: 5,
marginVertical: 5,
},
input: {
paddingHorizontal: 10,
alignItems: "center",
minHeight: 30,
},
errorText: {
color: "red",
},
component: {
marginTop: height*0.01,
}
});
borderColor: '#e8e8e8',
borderWidth: 1,
borderRadius: 5,
return (
<View style={styles.component}>
<View style={styles.container}>
<Controller
name={name}
control={control}
rules={{required: required, validate: (value) => true}}
render={({field: {onChange, onBlur, value }}) => (
<TextInput
secureTextEntry={secureTextEntry}
onChangeText={onChange}
onBlur={onBlur}
value={value}
placeholder={placeholder}
style={styles.input}/>
)}/>
</View>
{error && <Text style={styles.errorText}>{errorMessage}</Text>}
marginVertical: 5,
},
input: {
paddingHorizontal: 10,
alignItems: 'center',
minHeight: 30,
},
errorText: {
color: 'red',
},
component: {
marginTop: height * 0.01,
},
});
</View>
)
return (
<View style={styles.component}>
<View style={styles.container}>
<Controller
name={name}
control={control}
rules={{ required: required, validate: (value) => true }}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
secureTextEntry={secureTextEntry}
onChangeText={onChange}
onBlur={onBlur}
value={value}
placeholder={placeholder}
style={styles.input}
/>
)}
/>
</View>
{error && <Text style={styles.errorText}>{errorMessage}</Text>}
</View>
);
}
import React from 'react';
import { StyleSheet, TextInput } from 'react-native';
import { View } from '../components/Themed';
import {
FilterKeys,
FilterValues,
SortDirection,
SortKeys,
} from '../constants/filterOptions/interface';
import { genres, sortDirections, sortValues } from '../constants/filterOptions';
import SelectInput from './SelectInput';
import SearchInput from './SearchInput';
export type FilterPaneProps = {
onSearchChange: (value: string) => void;
onFilterChange: (key: FilterKeys, value: FilterValues) => void;
onSortChange: (value: SortKeys) => void;
onSortDirectionChange: (value: SortDirection) => void;
};
const FilterPane: React.FC<FilterPaneProps> = ({
onSearchChange,
onFilterChange,
onSortChange,
onSortDirectionChange,
}: FilterPaneProps) => {
return (
<View style={styles.container}>
<View style={styles.row}>
<View>
<SelectInput
defaultValue="Genres"
data={genres}
onChange={(value) => onFilterChange('genres', value ? [value] : [])}
/>
</View>
<View style={styles.inlineRow}>
<SelectInput
defaultValue="Order by"
data={sortValues}
onChange={(value) => onSortChange(value ? value : 'title')}
/>
<SelectInput
data={sortDirections}
onChange={(value) => onSortDirectionChange(value ? value : 'asc')}
/>
</View>
</View>
<View style={styles.row}>
<SearchInput
placeholder="Search for movie titles"
searchThreshold={3}
onChange={onSearchChange}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginTop: 96,
marginHorizontal: 16,
width: '95%',
display: 'flex',
},
row: {
flex: 1,
display: 'flex',
flexDirection: 'row',
paddingVertical: 4,
alignItems: 'center',
justifyContent: 'space-between',
},
inlineRow: {
display: 'flex',
flexDirection: 'row',
},
});
export default FilterPane;
import React, { useEffect, useState } from "react";
import { useForm, SubmitHandler, FormProvider } from "react-hook-form";
import { useDispatch, useSelector } from "react-redux";
import { signIn, signUp } from "../store/ducks/auth/actions";
import { ApplicationState } from "../store/interface";
import Logo from "../assets/images/cinema.png";
import { View, Text, Image, StyleSheet, useWindowDimensions, Button, Pressable } from "react-native";
import CustomInput from "./CustomInput";
import React, { useEffect, useState } from 'react';
import { useForm, SubmitHandler, FormProvider } from 'react-hook-form';
import { useDispatch, useSelector } from 'react-redux';
import { signIn, signUp } from '../store/ducks/auth/actions';
import { ApplicationState } from '../store/interface';
import Logo from '../assets/images/cinema.png';
import {
View,
Text,
Image,
StyleSheet,
useWindowDimensions,
Button,
Pressable,
} from 'react-native';
import CustomInput from './CustomInput';
export default function App() {
const {height} = useWindowDimensions();
const { height } = useWindowDimensions();
const methods = useForm<FormValues>({
defaultValues: {
email: "",
password: "",
email: '',
password: '',
},
mode: "onSubmit",
reValidateMode: "onBlur",
});
mode: 'onSubmit',
reValidateMode: 'onBlur',
});
const { formState: { errors }, handleSubmit } = methods;
const {
formState: { errors },
handleSubmit,
} = methods;
const onSubmit: SubmitHandler<FormValues> = (values: FormValues) => onFinish(values);
const onSubmit: SubmitHandler<FormValues> = (values: FormValues) =>
onFinish(values);
interface FormValues {
email: string;
password: string;
......@@ -33,42 +41,46 @@ const { formState: { errors }, handleSubmit } = methods;
}
const [isLogin, setIsLogin] = useState(true);
const dispatch = useDispatch();
const { loading, error } = useSelector(({ auth }: ApplicationState) => auth);
const dispatch = useDispatch();
const { loading, error } = useSelector(({ auth }: ApplicationState) => auth);
// Form completed
const onFinish = (values: FormValues) => {
// Form completed
const onFinish = (values: FormValues) => {
if (isLogin) {
dispatch(signIn({ email: values.email, password: values.password }));
} else {
dispatch(signUp({ email: values.email, username: values.username || "", password: values.password }));
dispatch(
signUp({
email: values.email,
username: values.username || '',
password: values.password,
})
);
}
};
};
// TODO If the redux state contains an error, display an error alert
useEffect(() => {
if (error) {
console.log("Show error");
console.log('Show error');
}
}, [error]);
const styles = StyleSheet.create({
root: {
alignItems: "center",
alignItems: 'center',
padding: 20,
},
logo: {
width: "70%",
width: '70%',
maxWidth: 300,
maxHeight: 200,
},
container: {
width: "100%",
width: '100%',
padding: 15,
marginVertical: 5,
alignItems: "center",
alignItems: 'center',
borderRadius: 5,
},
subText: {
......@@ -76,28 +88,62 @@ const { formState: { errors }, handleSubmit } = methods;
},
button: {
marginTop: 10,
}
})
},
});
return (
<View>
<Image source={Logo} style={[styles.logo, {height: height* 0.3}]} resizeMode="contain"/>
<FormProvider {...methods}>
{isLogin ? null : <CustomInput required={isLogin ? false : true} error={errors.username ? true : false} errorMessage="Du må fylle inn brukernavn" name="username" placeholder="username" /> }
<CustomInput required={true} error={errors.email ? true : false } errorMessage="Du må fylle inn e-post" name="email" placeholder="email" />
<CustomInput required={true} error={errors.password ? true : false } errorMessage="Passorder må oppfylle krav ..." name="password" secureTextEntry={true} placeholder="password" />
<View style={styles.button}>
<Button disabled={loading} onPress={handleSubmit(onSubmit)} title={isLogin ? "Login" : "Register"}/>
<Pressable onPress={() => {setIsLogin(!isLogin); methods.clearErrors()}} style={styles.container}>
<Text style={styles.subText}>
{!isLogin ? "Already have a user? Login." : "Need a new user? Register."}
</Text>
</Pressable>
</View>
</FormProvider>
<Image
source={Logo}
style={[styles.logo, { height: height * 0.3 }]}
resizeMode="contain"
/>
<FormProvider {...methods}>
{isLogin ? null : (
<CustomInput
required={isLogin ? false : true}
error={errors.username ? true : false}
errorMessage="Du må fylle inn brukernavn"
name="username"
placeholder="username"
/>
)}
<CustomInput
required={true}
error={errors.email ? true : false}
errorMessage="Du må fylle inn e-post"
name="email"
placeholder="email"
/>
<CustomInput
required={true}
error={errors.password ? true : false}
errorMessage="Passorder må oppfylle krav ..."
name="password"
secureTextEntry={true}
placeholder="password"
/>
<View style={styles.button}>
<Button
disabled={loading}
onPress={handleSubmit(onSubmit)}
title={isLogin ? 'Login' : 'Register'}
/>
<Pressable
onPress={() => {
setIsLogin(!isLogin);
methods.clearErrors();
}}
style={styles.container}
>
<Text style={styles.subText}>
{!isLogin
? 'Already have a user? Login.'
: 'Need a new user? Register.'}
</Text>
</Pressable>
</View>
</FormProvider>
</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 { useDispatch, useSelector } from 'react-redux';
import { fetchMovies } from '../store/ducks/movies/actions';
import { FetchMovieParams, MovieEntity } from '../store/ducks/movies/types';
import { ApplicationState } from '../store/interface';
import MovieCard from './MovieCard';
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";
const baseQuery: FetchMovieParams = {
perPage: 20,
page: 0,
orderBy: 'title',
order: 'desc',
filters: {
title: 'Al',
},
export type MovieTableProps = {
path: string;
query: FetchMovieParams;
movies: MovieEntity[];
currentPage: number;
onPageScroll: () => void;
};
const MovieTable = ({ path }: { path: string }): JSX.Element => {
const dispatch = useDispatch();
const MovieTable: React.FC<MovieTableProps> = ({
path,
query,
movies,
currentPage,
onPageScroll,
}: MovieTableProps) => {
const { data, documentCount, loading, error } = useSelector(
({ movies }: ApplicationState) => movies
);
const [movies, setMovies] = useState<MovieEntity[]>([]);
const [prevPageLoaded, setPageLoaded] = useState(0);
const fetchByPage = (page: number) => {
dispatch(fetchMovies({ ...baseQuery, page }));
};
// Fetch movies on mount
useEffect(() => {
dispatch(fetchMovies(baseQuery));
setPageLoaded(baseQuery.page);
}, []);
// Fetch movies on page change
useEffect(() => {
if (!error && !loading) {
// Ensure that we don't add duplicate movies to the array
const moviesToAdd = [...movies];
data.forEach((movie: MovieEntity) => {
if (!moviesToAdd.some((m) => m.id === movie.id)) {
moviesToAdd.push(movie);
}
});
setMovies([...moviesToAdd]);
}
}, [data]);
return (
<View style={styles.movieList}>
<FlatList
style={{ width: '100%', height: '100%' }}
columnWrapperStyle={{ flex: 1, justifyContent: 'space-around' }}
style={{ width: "100%", height: "100%" }}
columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }}
contentContainerStyle={styles.movieItems}
data={movies}
numColumns={2}
......@@ -65,17 +38,12 @@ const MovieTable = ({ path }: { path: string }): JSX.Element => {
// Prevent bug where onEndReached is called multiple times
if (distanceFromEnd < 0) return;
// Prevent fetching if all movies are loaded
if (
prevPageLoaded + 1 >=
Math.ceil(documentCount / baseQuery.perPage)
)
return;
fetchByPage(prevPageLoaded + 1);
setPageLoaded(prevPageLoaded + 1);
if (currentPage >= Math.ceil(documentCount / query.perPage)) return;
// Dispatch scroll event to parent component
onPageScroll();
}}
onEndReachedThreshold={1.5}
initialNumToRender={baseQuery.perPage}
initialNumToRender={query.perPage}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => <MovieCard movie={item} />}
ListFooterComponent={() => (
......@@ -88,20 +56,20 @@ const MovieTable = ({ path }: { path: string }): JSX.Element => {
const styles = StyleSheet.create({
movieItems: {
justifyContent: 'center',
justifyContent: "center",
flexGrow: 1 / 2,
backgroundColor: 'white',
backgroundColor: "white",
},
movieList: {
marginTop: 4,
width: '95%',
height: '100%',
display: 'flex',
alignItems: 'center',
textAlign: 'center',
width: "95%",
height: "95%",
display: "flex",
alignItems: "center",
textAlign: "center",
},
loading: {
fontWeight: 'bold',
fontWeight: "bold",
marginBottom: 8,
},
});
......