Commit 6adf420e authored by Erlend Hermanrud's avatar Erlend Hermanrud
Browse files

Resolve "Implementere rating av film"

parent 047dcb6c
import React from 'react';
import { View, Text, useThemeColor } from './common/Themed';
import { StyleSheet, Image } from 'react-native';
import moment from 'moment';
import { AirbnbRating } from 'react-native-ratings';
interface CommentProps {
date: string;
comment: string;
username: string;
rating: number;
}
const Comment: React.FC<CommentProps> = ({ date, comment, username, rating }: CommentProps) => {
return (
<View style={[styles.comment, { backgroundColor: useThemeColor({}, 'component') }]}>
<View style={[styles.row, { backgroundColor: useThemeColor({}, 'component') }]}>
<Text style={styles.dateText}>{moment(date).format('DD/MM/YYYY - HH:MM')}</Text>
<AirbnbRating
count={5}
defaultRating={rating}
showRating={false}
isDisabled={true}
size={15}
/>
</View>
<View style={[styles.leftRow, { backgroundColor: useThemeColor({}, 'component') }]}>
<Image
source={{
uri: `https://i.pravatar.cc/64?img=${Math.floor(Math.random() * 70)}`,
}}
style={styles.avatar}
/>
<Text style={styles.text}>{comment}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
comment: {
flexGrow: 1,
width: '100%',
marginVertical: 4,
padding: 4,
},
dateText: {
fontSize: 14,
},
leftRow: {
flexGrow: 1,
flexDirection: 'row',
},
row: {
flexGrow: 1,
flexDirection: 'row',
alignContent: 'space-around',
justifyContent: 'space-between',
},
avatar: {
width: 32,
height: 32,
borderRadius: 32,
},
text: {
marginLeft: 4,
fontSize: 14,
},
});
export default Comment;
import React from "react";
import { StyleSheet } from "react-native";
import { View } from "../components/Themed";
import { genres, sortValues } from "../constants/filterOptions";
import {
FilterKeys,
FilterValues,
SortDirection,
SortKeys,
} from "../constants/filterOptions/interface";
import { genres, sortDirections, sortValues } from "../constants/filterOptions";
import SelectInput from "./SelectInput";
import SearchInput from "./SearchInput";
import SearchInput from "./common/SearchInput";
import SelectInput from "./common/SelectInput";
import { View } from "./common/Themed";
import ToggleButton from "./ToggleButton";
export type FilterPaneProps = {
onSearchChange: (value: string) => void;
......@@ -37,13 +38,21 @@ const FilterPane: React.FC<FilterPaneProps> = ({
<View style={styles.inlineRow}>
<SelectInput
defaultValue="Order by"
style={{
borderRightWidth: 0,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
data={sortValues}
onChange={(value) => onSortChange(value ? value : "title")}
/>
<SelectInput
data={sortDirections}
style={{ marginRight: 4 }}
onChange={(value) => onSortDirectionChange(value ? value : "asc")}
<ToggleButton
style={{ borderBottomLeftRadius: 0, borderTopLeftRadius: 0 }}
onClick={(toggled) =>
onSortDirectionChange(toggled ? "asc" : "desc")
}
activeIcon="arrow-upward"
inactiveIcon="arrow-downward"
/>
</View>
</View>
......@@ -74,6 +83,27 @@ const styles = StyleSheet.create({
flexDirection: "row",
justifyContent: "flex-end",
},
sortOrderWrapper: {
height: 40,
width: 40,
zIndex: 1000,
borderRadius: 15,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderWidth: 1,
overflow: "hidden",
},
sortOrderButton: {
height: 40,
width: 40,
paddingBottom: 2,
alignItems: "center",
justifyContent: "center",
borderRadius: 15,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderWidth: 1,
},
});
export default FilterPane;
import React from "react";
import { View, Text } from "./Themed";
import { StyleSheet } from "react-native";
import React from 'react';
import { StyleSheet } from 'react-native';
import { Text, View } from './common/Themed';
interface KeyStatisticsItemProps {
title: string;
statistics: string;
}
export default function KeyStatisticsItem({
statistics,
title,
}: KeyStatisticsItemProps) {
export default function KeyStatisticsItem({ statistics, title }: KeyStatisticsItemProps) {
const styles = StyleSheet.create({
item: {
alignItems: "center",
width: "50%",
alignItems: 'center',
width: '50%',
marginBottom: 10,
},
});
return (
<View
style={styles.item}
lightColor="rgba(0,0,0)"
darkColor="rgba(255,255,255)"
>
<Text
style={styles.item}
lightColor="rgba(0,0,0,1)"
darkColor="rgba(255,255,255,1)"
>
<View style={styles.item} lightColor='rgba(0,0,0)' darkColor='rgba(255,255,255)'>
<Text style={styles.item} lightColor='rgba(0,0,0,1)' darkColor='rgba(255,255,255,1)'>
{title}
</Text>
<Text
style={styles.item}
lightColor="rgba(0,0,0,1)"
darkColor="rgba(255,255,255,1)"
>
<Text style={styles.item} lightColor='rgba(0,0,0,1)' darkColor='rgba(255,255,255,1)'>
{statistics}
</Text>
</View>
......
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 { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import {
Alert,
View,
Text,
ActivityIndicator,
Button,
Image,
Pressable,
StyleSheet,
Text,
useWindowDimensions,
Button,
Pressable,
ActivityIndicator,
View,
} from 'react-native';
import CustomInput from './CustomInput';
import AwesomeAlert from 'react-native-awesome-alerts';
import { useDispatch, useSelector } from 'react-redux';
import Logo from '../assets/images/cinema.png';
import { signIn, signUp } from '../store/ducks/auth/actions';
import { ApplicationState } from '../store/interface';
import CustomInput from './common/CustomInput';
export default function App() {
const { height } = useWindowDimensions();
......@@ -35,8 +34,7 @@ export default function App() {
handleSubmit,
} = methods;
const onSubmit: SubmitHandler<FormValues> = (values: FormValues) =>
onFinish(values);
const onSubmit: SubmitHandler<FormValues> = (values: FormValues) => onFinish(values);
interface FormValues {
email: string;
password: string;
......@@ -58,7 +56,7 @@ export default function App() {
email: values.email,
username: values.username || '',
password: values.password,
})
}),
);
}
};
......@@ -107,35 +105,31 @@ export default function App() {
return (
<View style={styles.root}>
<Image
source={Logo}
style={[styles.logo, { height: height * 0.3 }]}
resizeMode="contain"
/>
<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"
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"
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"
errorMessage='Passorder må oppfylle krav ...'
name='password'
secureTextEntry={true}
placeholder="password"
placeholder='password'
/>
<View style={styles.button}>
<Button
......@@ -151,9 +145,7 @@ export default function App() {
style={styles.container}
>
<Text style={styles.subText}>
{!isLogin
? 'Already have a user? Login.'
: 'Need a new user? Register.'}
{!isLogin ? 'Already have a user? Login.' : 'Need a new user? Register.'}
</Text>
</Pressable>
</View>
......@@ -166,12 +158,12 @@ export default function App() {
message={`Server responded with: ${alertMessage}`}
closeOnTouchOutside={true}
showConfirmButton={true}
confirmText="Ok"
confirmText='Ok'
onConfirmPressed={() => {
setAlertmessage('');
}}
/>
{loading && <ActivityIndicator size="large" color="#00ff00" />}
{loading && <ActivityIndicator size='large' color='#00ff00' />}
</View>
);
}
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { Image, StyleSheet, Text, View } from 'react-native';
import { Dimensions, Image, StyleSheet, Text } from 'react-native';
import { AirbnbRating } from 'react-native-ratings';
import { MovieEntity } from '../store/ducks/movies/types';
import { Dimensions } from 'react-native';
import { useThemeColor } from './Themed';
import { useNavigation } from '@react-navigation/native';
import { useThemeColor, View } from './common/Themed';
interface MovieCardProps {
movie: MovieEntity;
......@@ -53,7 +52,13 @@ const MovieCard = (props: MovieCardProps): JSX.Element => {
}}
style={[styles.poster, { width: iw(), height: ih() }]}
/>
<View style={[styles.cardInfo, { maxWidth: iw() }]}>
<View
style={[
styles.cardInfo,
{ maxWidth: iw() },
{ backgroundColor: useThemeColor({}, 'component') },
]}
>
<Text style={[styles.titleText, { color: useThemeColor({}, 'inputText') }]}>
{movie.title}
</Text>
......
......@@ -3,31 +3,40 @@ import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Button,
FlatList,
Image,
ScrollView,
StyleSheet,
useWindowDimensions,
} from 'react-native';
import AwesomeAlert from 'react-native-awesome-alerts';
import { useDispatch, useSelector } from 'react-redux';
import { MovieDetail } from '../models/movieDetail.model';
import { rateMovie } from '../store/ducks/auth/actions';
import { clearMovie, fetchMovieById } from '../store/ducks/movies/actions';
import { MovieState } from '../store/ducks/movies/types';
import { ApplicationState } from '../store/interface';
import Seperator from './common/Seperator';
import { Text, View } from './common/Themed';
import KeyStatisticsItem from './KeyStatisticsItem';
import Pill from './Pill';
import Seperator from './Seperator';
import { Text, View } from './Themed';
import RatingModal from './RatingModal';
import Comment from './Comment';
interface MovieInfoProps {
movieID: string;
}
const noAlert = {
title: '',
message: '',
error: false,
};
export default function MovieInfo({ movieID }: MovieInfoProps) {
const { height } = useWindowDimensions();
const styles = StyleSheet.create({
root: {
padding: 20,
flex: 1,
},
logo: {
flex: 1,
......@@ -61,12 +70,52 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
marginTop: 15,
paddingBottom: '1rem',
},
comments: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
marginBottom: 32,
},
alert: {
width: 400,
},
alertError: {
color: 'red',
},
});
const dispatch = useDispatch();
const data = useSelector(({ movies }: ApplicationState) => movies.byId);
const { ratings, token, loading, error } = useSelector((state: ApplicationState) => state.auth);
const [rateModalVisible, setRateModalVisible] = useState(false);
const [awaitingRating, setAwaitingRating] = useState(false);
const [alert, setAlert] = useState({ ...noAlert });
/**
* Callback for rating modal
* @param rating number of stars
* @param comment optional comment
*/
const dispatchRating = (rating: number, comment: string) => {
if (token) {
// If user is logged in, dispatch rateMovie
setAwaitingRating(true);
dispatch(rateMovie({ token: token, movieId: movieID, rating, comment }));
} else {
// If user is not logged in, dispatch error alert
setAwaitingRating(false);
setAlert({
title: 'Error',
message: 'User not logged in',
error: true,
});
}
};
// Fetch movies on load
useEffect(() => {
dispatch(fetchMovieById({ id: movieID }));
return () => {
......@@ -74,51 +123,120 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
};
}, []);
if (data) {
useEffect(() => {
// If we're not loading and awaitingRating is true, we sucessfully rated movie => Show sucess alert
if (!loading && awaitingRating && ratings) {
dispatch(fetchMovieById({ id: movieID }));
setAwaitingRating(false);
setAlert({
title: 'Rating succesful',
message: 'You have succesfully rated this movie',
error: false,
});
}
}, [ratings]);
useEffect(() => {
// If an error occurs, show an error alert with the error message returned from the server
if (error) {
setAwaitingRating(false);
setAlert({
title: 'Error',
message: 'Server responded with: ' + error,
error: true,
});
}
}, [error]);
if (loading || awaitingRating) {
return (
<View style={{ flex: 1, alignContent: 'center', alignItems: 'center', height: '100%' }}>
<ActivityIndicator size='large' color='#00ff00' />
</View>
);
} else if (data) {
return (
<ScrollView style={styles.root}>
<View style={{ alignItems: 'center' }}>
<Image
source={{
uri: data.movie.poster,
}}
style={[styles.logo, { height: height * 0.5 }]}
resizeMode='contain'
/>
</View>
<View style={{ alignItems: 'center' }}>
<Text>Stars -{'>'} To be implemented</Text>
</View>
<View>
<Button onPress={() => console.log('To be implemented')} title={'Give rating'} />
</View>
<View style={styles.key_number_container}>
<KeyStatisticsItem title='RUNTIME' statistics={String(data.movie.runtime)} />
<KeyStatisticsItem title='IMDG-RATING' statistics={String(data.movie.imdbRating)} />
<KeyStatisticsItem title='RATING' statistics={String(data.movie.rating)} />
<KeyStatisticsItem title='PGA-RATING' statistics={String(data.movie.rated)} />
</View>
<Seperator />
<View>
<Text style={{ fontSize: 22 }}>{data.movie.year}</Text>
<Text style={{ fontSize: 18 }}>{data.movie.title}</Text>
</View>
<Seperator />
<Text>{data.movie.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>
<Text>Reviews -{'>'} To be implemented</Text>
</View>
</ScrollView>
<View style={{ flex: 1, alignItems: 'center' }}>
<ScrollView style={styles.root}>
<View style={{ alignItems: 'center' }}>
<Image
source={{
uri: data.movie.poster,
}}
style={[styles.logo, { height: height * 0.5 }]}
resizeMode='contain'
/>
</View>
<View style={{ alignItems: 'center' }}>
<Text>Stars -{'>'} To be implemented</Text>
</View>
<View>
<Button
onPress={() => setRateModalVisible(true)}
title={'Give rating'}
disabled={(() => {
return ratings.find((rating) => rating.movie.id === movieID) ? true : false;
})()}
/>
</View>
<View style={styles.key_number_container}>
<KeyStatisticsItem title='RUNTIME' statistics={String(data.movie.runtime)} />
<KeyStatisticsItem title='IMDG-RATING' statistics={String(data.movie.imdbRating)} />
<KeyStatisticsItem title='RATING' statistics={String(data.movie.rating)} />
<KeyStatisticsItem title='PGA-RATING' statistics={String(data.movie.rated)} />
</View>
<Seperator />
<View>
<Text style={{ fontSize: 22 }}>{data.movie.year}</Text>
<Text style={{ fontSize: 18 }}>{data.movie.title}</Text>
</View>
<Seperator />
<Text>{data.movie.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 />
<RatingModal
modalVisible={rateModalVisible}
onClose={() => setRateModalVisible(false)}
onSubmit={dispatchRating}
></RatingModal>
<View style={styles.comments}>
{data.ratings.map((rating) => {
return (
<Comment
date={rating.date}
comment={rating.comment}
username={rating.user.username}
rating={rating.rating}
key={rating.user.id}
></Comment>
);
})}
</View>
</ScrollView>
<AwesomeAlert
contentContainerStyle={styles.alert}
titleStyle={alert.error ? styles.alertError : {}}
show={alert.title.length > 0}
title={alert.title}