Commit 38a70391 authored by Erlend Hermanrud's avatar Erlend Hermanrud
Browse files

Resolve "Styling og dokumentasjon"

parent e5949bef
import React from 'react';
import { View, Text, useThemeColor } from './common/Themed';
import { StyleSheet, Image } from 'react-native';
import moment from 'moment';
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import { AirbnbRating } from 'react-native-ratings';
import { Text, useThemeColor, View } from './common/Themed';
interface CommentProps {
date: string;
......@@ -11,11 +11,21 @@ interface CommentProps {
rating: number;
}
/**
* Component for single comments (used in a FlatList or similar to render comment section)
* Contains a username, date, comment and rating along with a randomly generated avatar url.
*/
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>
<View style={styles.headerView}>
<Text style={styles.usernameText} numberOfLines={1} ellipsizeMode='tail'>
{username}
</Text>
<Text style={styles.dateText}>{'| ' + moment(date).format('DD/MM/YY - HH:MM')}</Text>
</View>
<AirbnbRating
count={5}
defaultRating={rating}
......@@ -31,7 +41,9 @@ const Comment: React.FC<CommentProps> = ({ date, comment, username, rating }: Co
}}
style={styles.avatar}
/>
<Text style={styles.text}>{comment}</Text>
<Text style={styles.text} numberOfLines={3} ellipsizeMode='tail'>
{comment}
</Text>
</View>
</View>
);
......@@ -39,13 +51,25 @@ const Comment: React.FC<CommentProps> = ({ date, comment, username, rating }: Co
const styles = StyleSheet.create({
comment: {
flexGrow: 1,
flex: 1,
width: '100%',
marginVertical: 4,
padding: 4,
maxHeight: 90,
borderRadius: 6,
},
headerView: {
flexDirection: 'row',
flexGrow: 1,
backgroundColor: 'transparent',
},
dateText: {
fontSize: 14,
fontSize: 12,
},
usernameText: {
marginRight: 4,
fontSize: 12,
maxWidth: 100,
},
leftRow: {
flexGrow: 1,
......@@ -55,7 +79,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
flexDirection: 'row',
alignContent: 'space-around',
justifyContent: 'space-between',
alignItems: 'flex-start',
},
avatar: {
width: 32,
......@@ -63,8 +87,10 @@ const styles = StyleSheet.create({
borderRadius: 32,
},
text: {
textAlign: 'left',
marginLeft: 4,
fontSize: 14,
flex: 1,
},
});
......
import React from "react";
import { StyleSheet } from "react-native";
import { genres, sortValues } from "../constants/filterOptions";
import React from 'react';
import { StyleSheet } from 'react-native';
import { genres, sortValues } from '../constants/filterOptions';
import {
FilterKeys,
FilterValues,
SortDirection,
SortKeys,
} from "../constants/filterOptions/interface";
import SearchInput from "./common/SearchInput";
import SelectInput from "./common/SelectInput";
import { View } from "./common/Themed";
import ToggleButton from "./ToggleButton";
} from '../constants/filterOptions/interface';
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;
......@@ -19,6 +20,9 @@ export type FilterPaneProps = {
onSortDirectionChange: (value: SortDirection) => void;
};
/**
* FilterPane Component for displaying the different Movie Selection filters
*/
const FilterPane: React.FC<FilterPaneProps> = ({
onSearchChange,
onFilterChange,
......@@ -30,35 +34,33 @@ const FilterPane: React.FC<FilterPaneProps> = ({
<View style={styles.row}>
<View>
<SelectInput
defaultValue="All genres"
defaultValue='All genres'
data={genres}
onChange={(value) => onFilterChange("genres", value ? [value] : [])}
onChange={(value) => onFilterChange('genres', value ? [value] : [])}
/>
</View>
<View style={styles.inlineRow}>
<SelectInput
defaultValue="Order by"
defaultValue='Order by'
style={{
borderRightWidth: 0,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
}}
data={sortValues}
onChange={(value) => onSortChange(value ? value : "title")}
onChange={(value) => onSortChange(value ? value : 'title')}
/>
<ToggleButton
style={{ borderBottomLeftRadius: 0, borderTopLeftRadius: 0 }}
onClick={(toggled) =>
onSortDirectionChange(toggled ? "asc" : "desc")
}
activeIcon="arrow-upward"
inactiveIcon="arrow-downward"
onClick={(toggled) => onSortDirectionChange(toggled ? 'asc' : 'desc')}
activeIcon='arrow-upward'
inactiveIcon='arrow-downward'
/>
</View>
</View>
<View style={styles.row}>
<SearchInput
placeholder="Search for movie titles"
placeholder='Search for movie titles'
searchThreshold={3}
onChange={onSearchChange}
/>
......@@ -69,19 +71,19 @@ const FilterPane: React.FC<FilterPaneProps> = ({
const styles = StyleSheet.create({
container: {
width: "100%",
paddingVertical: "2%",
width: '100%',
paddingVertical: '2%',
},
row: {
flexDirection: "row",
flexDirection: 'row',
paddingVertical: 5,
justifyContent: "space-between",
justifyContent: 'space-between',
},
inlineRow: {
flexShrink: 1,
flexGrow: 0,
flexDirection: "row",
justifyContent: "flex-end",
flexDirection: 'row',
justifyContent: 'flex-end',
},
sortOrderWrapper: {
height: 40,
......@@ -91,14 +93,14 @@ const styles = StyleSheet.create({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderWidth: 1,
overflow: "hidden",
overflow: 'hidden',
},
sortOrderButton: {
height: 40,
width: 40,
paddingBottom: 2,
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
justifyContent: 'center',
borderRadius: 15,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
......
......@@ -6,6 +6,10 @@ interface KeyStatisticsItemProps {
title: string;
statistics: string;
}
/**
* Component for showing a key statistics item with a title and value
*/
export default function KeyStatisticsItem({ statistics, title }: KeyStatisticsItemProps) {
const styles = StyleSheet.create({
item: {
......
......@@ -34,6 +34,7 @@ export default function App() {
handleSubmit,
} = methods;
// Handle form submit
const onSubmit: SubmitHandler<FormValues> = (values: FormValues) => onFinish(values);
interface FormValues {
email: string;
......@@ -49,8 +50,10 @@ export default function App() {
// Form completed
const onFinish = (values: FormValues) => {
if (isLogin) {
// If the form is in loginMode, dispatch the signIn action
dispatch(signIn({ email: values.email, password: values.password }));
} else {
// If the form is in signUpMode, dispatch the signUp action
dispatch(
signUp({
email: values.email,
......@@ -61,11 +64,12 @@ export default function App() {
}
};
// TODO If the redux state contains an error, display an error alert
useEffect(() => {
// If there is an error, show the alert
if (error) {
setAlertmessage(error);
} else {
// If the error state updated and no error was found, clear the alert
setAlertmessage('');
}
}, [error]);
......
import { useNavigation } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { Dimensions, Image, StyleSheet, Text } from 'react-native';
import { Dimensions, Image, StyleSheet, Text, TouchableOpacity } from 'react-native';
import { AirbnbRating } from 'react-native-ratings';
import { MovieEntity } from '../store/ducks/movies/types';
import { useThemeColor, View } from './common/Themed';
......@@ -18,14 +18,18 @@ const MovieCard = (props: MovieCardProps): JSX.Element => {
const { movie } = props;
const [dimensions, setDimensions] = useState({ window, screen });
// Dynamically calculate the width of the card based on the screen size
const iw = () => {
return dimensions.window.width / 2 - 20;
};
// Dynamically calculate the height of the card based on the screen size
const ih = () => {
return (dimensions.window.width / 2 - 20) * (443 / 300);
};
// Once the dimensions are set, we can use them to calculate the height/width of the card
useEffect(() => {
Dimensions.addEventListener('change', ({ window, screen }) => {
setDimensions({ window, screen });
......@@ -34,22 +38,22 @@ const MovieCard = (props: MovieCardProps): JSX.Element => {
const navigation = useNavigation();
// When the user clicks on a movie card, we navigate to the movie page
const navigateToMovie = () => {
navigation.navigate('Movie', {
movieTitle: movie.title,
movieID: movie.id,
});
};
return (
<View
style={[styles.card, { backgroundColor: useThemeColor({}, 'component') }]}
onTouchEnd={navigateToMovie}
>
<View style={[styles.card, { backgroundColor: useThemeColor({}, 'component') }]}>
<TouchableOpacity onPress={navigateToMovie}>
<View style={{ flexGrow: 1 }}>
<Image
source={{
uri: movie.poster,
}}
resizeMode='cover'
style={[styles.poster, { width: iw(), height: ih() }]}
/>
<View
......@@ -71,6 +75,8 @@ const MovieCard = (props: MovieCardProps): JSX.Element => {
/>
</View>
</View>
</TouchableOpacity>
</View>
);
};
......@@ -95,7 +101,7 @@ const styles = StyleSheet.create({
shadowOffset: { width: -2, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 3,
elevation: 1,
elevation: 3,
},
titleText: {
textAlign: 'center',
......
......@@ -2,23 +2,23 @@ import * as React from 'react';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Button,
FlatList,
Image,
ScrollView,
StyleSheet,
useWindowDimensions,
} from 'react-native';
import AwesomeAlert from 'react-native-awesome-alerts';
import { Button, Icon } from 'react-native-elements';
import { AirbnbRating } from 'react-native-ratings';
import { default as AwesomeIcon } from 'react-native-vector-icons/FontAwesome';
import { useDispatch, useSelector } from 'react-redux';
import { rateMovie } from '../store/ducks/auth/actions';
import { clearMovie, fetchMovieById } from '../store/ducks/movies/actions';
import { ApplicationState } from '../store/interface';
import Comment from './Comment';
import Seperator from './common/Seperator';
import { Text, View } from './common/Themed';
import KeyStatisticsItem from './KeyStatisticsItem';
import RatingModal from './RatingModal';
import Comment from './Comment';
interface MovieInfoProps {
movieID: string;
......@@ -31,17 +31,46 @@ const noAlert = {
};
export default function MovieInfo({ movieID }: MovieInfoProps) {
const { height } = useWindowDimensions();
const { height, width } = useWindowDimensions();
const styles = StyleSheet.create({
scrollView: {
flexGrow: 1,
width: '90%',
},
root: {
padding: 20,
flex: 1,
alignItems: 'center',
},
logo: {
flex: 1,
width: 200,
height: 350,
infoView: {
marginVertical: 10,
flexDirection: 'row',
alignItems: 'flex-start',
alignContent: 'space-between',
},
infoRow: {
flexGrow: 1,
height: '100%',
alignItems: 'center',
textAlign: 'center',
},
poster: {
borderRadius: 5,
},
titleText: {
flexGrow: 1,
fontSize: 18,
fontWeight: 'bold',
},
infoText: {
fontSize: 16,
fontWeight: 'bold',
},
icon: { fontSize: 22, marginRight: 10 },
maxRow: {
flexGrow: 1,
flexDirection: 'row',
alignItems: 'center',
},
container: {
width: '100%',
......@@ -71,7 +100,7 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
paddingBottom: '1rem',
},
comments: {
flexGrow: 1,
width: '100%',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
......@@ -148,6 +177,13 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
}
}, [error]);
// Function that formats minutes ot hours and minutes
const formatTime = (time: number) => {
const hours = Math.floor(time / 60);
const minutes = time % 60;
return hours + 'h ' + minutes + 'm';
};
if (loading || awaitingRating) {
return (
<View style={{ flex: 1, alignContent: 'center', alignItems: 'center', height: '100%' }}>
......@@ -156,51 +192,111 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
);
} else if (data) {
return (
<View style={{ flex: 1, alignItems: 'center' }}>
<ScrollView style={styles.root}>
<View style={{ alignItems: 'center' }}>
<View style={styles.root}>
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
<View style={{ flexGrow: 1, alignItems: 'center' }}>
<View style={styles.infoView}>
<View style={{ flexGrow: 1 }}>
<Image
source={{
uri: data.movie.poster,
}}
style={[styles.logo, { height: height * 0.5 }]}
style={[styles.poster, { width: width * 0.45, height: (width * 0.45 * 89) / 60 }]}
resizeMode='contain'
/>
</View>
<View style={{ alignItems: 'center' }}>
<Text>Stars -{'>'} To be implemented</Text>
<View style={styles.infoRow}>
<Text
style={[styles.titleText, { textAlign: 'center', maxWidth: 150, fontSize: 16 }]}
>
{data.movie.title}
</Text>
<View style={{ flexGrow: 1 }}>
<AirbnbRating
count={5}
defaultRating={data.movie.rating}
showRating={false}
isDisabled={true}
size={25}
/>
<Text style={[styles.infoText, { textAlign: 'center' }]}>
{data.movie.rating.toFixed(2)} / 5
</Text>
</View>
<View style={styles.maxRow}>
<Icon
name='clock'
type='fontisto'
color='#517fa4'
style={styles.icon}
tvParallaxProperties={undefined}
/>
<Text style={styles.infoText}>{formatTime(data.movie.runtime)}</Text>
</View>
<View style={styles.maxRow}>
<Icon
name='calendar'
type='antdesign'
color='#517fa4'
style={styles.icon}
tvParallaxProperties={undefined}
/>
<Text style={styles.infoText}>{new Date(data.movie.released).getFullYear()}</Text>
</View>
<View style={styles.maxRow}>
<Text style={{ fontSize: 16 }}>PG-RATED: </Text>
<Text style={styles.infoText}>{data.movie.rated}</Text>
</View>
<View style={styles.maxRow}>
<Text style={{ fontSize: 16 }}>IMDB RATING: </Text>
<Text style={styles.infoText}>{data.movie.imdbRating}</Text>
</View>
</View>
</View>
<View>
<View style={{ width: 150, marginBottom: 10 }}>
<Button
onPress={() => setRateModalVisible(true)}
title={'Give rating'}
title=' Give Rating'
disabled={(() => {
return ratings.find((rating) => rating.movie.id === movieID) ? true : false;
})()}
icon={<AwesomeIcon name='star-o' size={24} color='white' />}
/>
</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 />
<Text style={[styles.titleText, { flexGrow: 0 }]}>Plot</Text>
<Seperator marginVertical={10} width='75%' curved />
<Text style={{ fontSize: 16, fontStyle: 'italic', textAlign: 'center' }}>
{data.movie.plot}
</Text>
<Seperator marginVertical={10} width={'15%'} curved />
<Text style={[styles.titleText, { flexGrow: 0 }]}>Cast</Text>
<Seperator marginVertical={10} curved />
<View style={{ width: '100%' }}>
{LabelAndText('DIRECTORS', 'Joshua King')}
<Seperator />
<Seperator marginVertical={10} curved />
{LabelAndText('PRODUCTION', 'Paramount Pictures, W365')}
<Seperator />
<Seperator marginVertical={10} curved />
{LabelAndText('WRITERS', 'Phil Hay, Matt Manfredi, Peter Chung')}
<Seperator />
{LabelAndText('STARRING ACTORS', 'Charlize Theron, Frances McDormand, Sophie Okonedo')}
<Seperator />
<Seperator marginVertical={10} curved />
{LabelAndText(
'STARRING ACTORS',
'Charlize Theron, Frances McDormand, Sophie Okonedo',
)}
<Seperator marginVertical={10} curved />
</View>
<Text style={[styles.titleText, { flexGrow: 0 }]}>Reviews</Text>
<Seperator marginVertical={10} width='75%' curved />
<RatingModal
modalVisible={rateModalVisible}
......@@ -221,6 +317,7 @@ export default function MovieInfo({ movieID }: MovieInfoProps) {
);
})}
</View>
</View>
</ScrollView>
<AwesomeAlert
......@@ -247,9 +344,9 @@ function LabelAndText(label: string, text: string) {
textTransform: 'capitalize',
fontWeight: 'bold',
marginRight: 5,
textAlign: 'left',
},
container: {
flex: 1,
flexWrap: 'wrap',
flexDirection: 'row',
},
......
......@@ -15,6 +15,9 @@ export type MovieTableProps = {
onPageScroll: () => void;
};
/**
* Component for rendering the scrollable list of movies.
*/
const MovieTable: React.FC<MovieTableProps> = ({
path,
query,
......@@ -30,6 +33,8 @@ const MovieTable: React.FC<MovieTableProps> = ({
const [isFull, setIsfull] = useState(false);
useEffect(() => {
// if the graphql query returns less movies than the page size,