Commit a61dfa07 authored by Thor-Herman's avatar Thor-Herman
Browse files

Merge branch 'refactor' into 'master'

Squashed commit of the following:

commit 1eb6f326
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 21:21:36 2020 +0100

    Minor style changes to Moviepage #15

commit ca6d9ea4
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 21:16:01 2020 +0100

    Run prettier on project #15

commit 46f18fd8
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 21:13:54 2020 +0100

    Rename pages folder to screens #15

commit eea65fd8
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 21:06:35 2020 +0100

    Remove to and from properties from filter #15

commit a3dd8f47
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 21:04:12 2020 +0100

    Add more comments #14

commit 810187c4
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:58:27 2020 +0100

    Add urls as variables #15

commit f65242e4
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:58:15 2020 +0100

    Rename pages to screens #15

commit d1661799
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:55:34 2020 +0100

    Add comments and remove imports #14 #15

commit 68104783
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:55:25 2020 +0100

    Delete unused files #15

commit 58d35bf8
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:49:01 2020 +0100

    Add comments and remove unused code for reducers #15

commit a45f7405
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:42:57 2020 +0100

    Remove unnecessary imports #15

commit 6d85c46f
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:37:18 2020 +0100

    Remove history #15

commit 8b5a9281
Author: Thor-Herman <44784559+Thor-Herman@users.noreply.github.com>
Date:   Fri Nov 13 20:36:53 2020 +0100

    Add comments to types #14 #15
parent b6223b73
import 'react-native';
import React from 'react';
import App from '../App.tsx';
import "react-native";
import React from "react";
import App from "../App.tsx";
// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';
import renderer from "react-test-renderer";
it('renders correctly', () => {
it("renders correctly", () => {
renderer.create(<App />);
});
import {
CHANGE_ORDERING,
OrderingAction,
OrderingActionPayload,
} from '../types/ordering';
import AppThunk from '../types';
import { searchMovies, searchMovieTitles } from './movieActions';
import { CHANGE_FILTER, FilterAction, FilterFormData } from '../types/filter';
import _ from 'lodash';
export const applyFilter = (action: OrderingAction | FilterAction): AppThunk => (
dispatch,
getState
) => {
dispatch(action);
return new Promise((resolve) => {
dispatch(searchMovies(true));
resolve();
});
};
import { CHANGE_ORDERING, OrderingActionPayload } from "../types/ordering";
import { CHANGE_FILTER, FilterFormData } from "../types/filter";
import _ from "lodash";
export const changeOrdering = (newOrder: OrderingActionPayload) => {
return { type: CHANGE_ORDERING, payload: newOrder };
......@@ -28,9 +11,8 @@ export const changeFilters = (data: FilterFormData) => {
type: CHANGE_FILTER,
payload: {
genres: {
..._.omit(data, ['to', 'from']), // Will return every genre and not to and from properties
...data, // Will return every genre and not to and from properties.
},
year: { to: data.to, from: data.from },
},
};
};
export * from './movieActions';
export * from './filterActions';
export * from './pageActions';
\ No newline at end of file
export * from "./movieActions";
export * from "./filterActions";
export * from "./pageActions";
import AppThunk from '../types';
import {ADD_MOVIE, SEARCH_TITLES, Movie, MovieActionTypes, UPDATE_MOVIES, MoviePage, ADD_MOVIES} from "../types/movies";
import AppThunk from "../types";
import {
ADD_MOVIE,
SEARCH_TITLES,
Movie,
MovieActionTypes,
UPDATE_MOVIES,
MoviePage,
ADD_MOVIES,
} from "../types/movies";
import axiosREST from "../api/axiosREST";
import {updatePages} from "./pageActions";
import {decideFilters} from "./utility/decideFilters";
import { updatePages } from "./pageActions";
import { decideFilters } from "./utility/decideFilters";
// Add a single movie to the current collection
export const addMovie = (movie: Movie): MovieActionTypes => {
return {
type: ADD_MOVIE,
payload: movie
}
return {
type: ADD_MOVIE,
payload: movie,
};
};
// Replaces what movies are stored
export const updateMovies = (movies: Array<Movie>): MovieActionTypes => {
return {
type: UPDATE_MOVIES,
payload: movies,
}
}
return {
type: UPDATE_MOVIES,
payload: movies,
};
};
// Adds movies to currently stored
export const addMovies = (movies: Array<Movie>): MovieActionTypes => {
return {
type: ADD_MOVIES,
payload: movies,
}
}
return {
type: ADD_MOVIES,
payload: movies,
};
};
// Updates search term
export const searchMovieTitles = (searchTerm: string): MovieActionTypes => {
return {
type: SEARCH_TITLES,
payload: searchTerm
}
return {
type: SEARCH_TITLES,
payload: searchTerm,
};
};
// Fetches a single movie
export const fetchMovie = (id: number): AppThunk => {
return async (dispatch) => {
const response = await axiosREST.get<Movie>(`/movies/${id}`);
dispatch(addMovie(response.data));
};
return async (dispatch) => {
const response = await axiosREST.get<Movie>(`/movies/${id}`);
dispatch(addMovie(response.data));
};
};
export const searchMovies = (resetPages = false, update = true): AppThunk => {
return async (dispatch, getState) => {
const filtering = getState().filtering;
const searchTerm = getState().movies.searchTerm;
const currentPage = resetPages ? 1 : getState().page.current;
const filters = decideFilters(filtering);
const query = `/movies?search=${searchTerm}${filters}&page=${currentPage}`
const response = await axiosREST.get<MoviePage>(query);
if (update) {
dispatch(updateMovies(response.data.results));
}
else {
dispatch(addMovies(response.data.results));
}
dispatch(updatePages(response.data.count, currentPage));
// Executes a REST query to backend with the current search term and filters
export const searchMovies = (resetPages = false, update = true): AppThunk => {
return async (dispatch, getState) => {
const filtering = getState().filtering;
const searchTerm = getState().movies.searchTerm;
const currentPage = resetPages ? 1 : getState().page.current;
const filters = decideFilters(filtering);
const query = `/movies?search=${searchTerm}${filters}&page=${currentPage}`;
const response = await axiosREST.get<MoviePage>(query);
if (update) {
// Should current selection be replaced or added to?
dispatch(updateMovies(response.data.results));
} else {
// This is the case when loading more results
dispatch(addMovies(response.data.results));
}
};
\ No newline at end of file
dispatch(updatePages(response.data.count, currentPage));
};
};
import {CHANGE_PAGE, PAGE_SIZE, PageActions, UPDATE_TOTAL_PAGES} from "../types/page";
import {
CHANGE_PAGE,
PAGE_SIZE,
PageActions,
UPDATE_TOTAL_PAGES,
} from "../types/page";
import AppThunk from "../types";
import {searchMovies} from "./movieActions";
import { searchMovies } from "./movieActions";
export const changePage = (newCurrent: number, next: number | null, prev: number | null): PageActions => {
return {
type: CHANGE_PAGE,
payload: {current: newCurrent, next, prev}
}
export const changePage = (
newCurrent: number,
next: number | null,
prev: number | null
): PageActions => {
return {
type: CHANGE_PAGE,
payload: { current: newCurrent, next, prev },
};
};
export const updatePages = (count: number, current: number): PageActions => {
const total = Math.ceil(count / PAGE_SIZE);
return {
type: UPDATE_TOTAL_PAGES,
payload: {total, current},
}
const total = Math.ceil(count / PAGE_SIZE);
return {
type: UPDATE_TOTAL_PAGES,
payload: { total, current },
};
};
export const newPage = (newCurrent: number, total: number): AppThunk => (dispatch, getState) => {
const next = newCurrent < total ? newCurrent + 1 : null;
const prev = newCurrent > 1 ? newCurrent - 1 : null;
dispatch(changePage(newCurrent, next, prev));
return new Promise((resolve) => {
dispatch(searchMovies(false, false));
resolve();
})
};
\ No newline at end of file
export const newPage = (newCurrent: number, total: number): AppThunk => (
dispatch
) => {
const next = newCurrent < total ? newCurrent + 1 : null;
const prev = newCurrent > 1 ? newCurrent - 1 : null;
dispatch(changePage(newCurrent, next, prev));
return new Promise((resolve) => {
// Done in order to dispatch sequentially
dispatch(searchMovies(false, false));
resolve();
});
};
import { FilteringState, Genres } from '../../types/filter';
import _ from 'lodash';
import { FilteringState, Genres } from "../../types/filter";
import _ from "lodash";
// Helper method. Converts the selected filters to REST query
export const decideFilters = ({ filter, ordering }: FilteringState) => {
let returnString = '';
if (filter.year.to && filter.year.from) {
returnString += `&year__gte=${filter.year.from}&year__lte=${filter.year.to}`;
}
let returnString = "";
const regex = decideRegex(filter.genres);
returnString += `&genre__regex=${regex}`;
if (ordering) {
const orderingPrefix = ordering.order === 'asc' ? '' : '-'; // Django needs - to sort desc
const orderingPrefix = ordering.order === "asc" ? "" : "-"; // Django needs - to sort desc
returnString += `&ordering=${orderingPrefix}${ordering.key}`;
}
return returnString;
};
// Helper method. Decides the RegEx in REST query for genres
const decideRegex = (filterGenres: { [key in Genres]: boolean }) => {
let regex = '(';
const checkedGenres = _.keys(_.pickBy(filterGenres, (genre: boolean) => genre))
let regex = "(";
const checkedGenres = _.keys(
_.pickBy(filterGenres, (genre: boolean) => genre)
);
for (let i = 0; i < checkedGenres.length; i++) {
regex += checkedGenres[i].toString();
regex += i === checkedGenres.length - 1 ? '' : '|'; // Last element, don't add final |
regex += i === checkedGenres.length - 1 ? "" : "|"; // Last element, don't add final |
}
regex += ')';
regex += ")";
return regex;
};
import axios from 'axios';
import axios from "axios";
import Constants from "expo-constants";
const { manifest } = Constants;
// This gets the IP address of the host for Expo client. Makes you connect to the IP address of the machine on LAN
const url = `http://${manifest.debuggerHost.split(':').shift()}:8000`;
const Localurl = `http://${manifest.debuggerHost.split(":").shift()}:8000`;
const VMUrl = "http://it2810-75.idi.ntnu.no:8000";
const prod = { // The VM backend
url: 'http://it2810-75.idi.ntnu.no:8000',
const prod = {
// The VM backend
url: VMUrl,
};
const dev = { // Local backend
url,
const dev = {
// Local backend
url: Localurl, // If trying the server locally. Change to Localurl.
};
export const config = process.env.NODE_ENV === 'development' ? dev : prod;
export const config = process.env.NODE_ENV === "development" ? dev : prod;
export default axios.create({baseURL: config.url});
export default axios.create({ baseURL: config.url });
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
presets: ["babel-preset-expo"],
};
};
import BrowsePage from '../pages/BrowsePage';
import LandingPage from '../pages/LandingPage';
import MoviePage from '../pages/MoviePage';
import React from 'react';
import { View } from 'react-native';
import { useFonts } from 'expo-font';
import { Ionicons } from '@expo/vector-icons';
import { Container, Footer, Icon, Input, Item, Left, Text } from 'native-base';
import SearchBar from './SearchBar';
import Header from './Header';
import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import FilterPage from '../pages/FilterPage';
import BrowseScreen from "../screens/BrowseScreen";
import MovieScreen from "../screens/MovieScreen";
import React from "react";
import { View } from "react-native";
import { useFonts } from "expo-font";
import { Ionicons } from "@expo/vector-icons";
import { Container, Footer, Icon, Input, Item, Left, Text } from "native-base";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import FilterPage from "../screens/FilterScreen";
export type StackParamList = {
// The parameters for each screen
Browse: undefined;
Movie: { id: number };
Filter: undefined,
Filter: undefined;
};
const Stack = createStackNavigator<StackParamList>();
const Stack = createStackNavigator<StackParamList>(); // For navigating screens
const App = () => {
let [fontsLoaded] = useFonts({
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
// Fonts used by Native Base
Roboto: require("native-base/Fonts/Roboto.ttf"),
Roboto_medium: require("native-base/Fonts/Roboto_medium.ttf"),
...Ionicons.font,
});
......@@ -34,10 +33,10 @@ const App = () => {
return (
<NavigationContainer>
<Container>
<Stack.Navigator initialRouteName='Browse'>
<Stack.Screen name='Browse' component={BrowsePage} />
<Stack.Screen name='Movie' component={MoviePage} />
<Stack.Screen name='Filter' component={FilterPage} />
<Stack.Navigator initialRouteName="Browse">
<Stack.Screen name="Browse" component={BrowseScreen} />
<Stack.Screen name="Movie" component={MovieScreen} />
<Stack.Screen name="Filter" component={FilterPage} />
</Stack.Navigator>
</Container>
</NavigationContainer>
......
import _ from "lodash";
import { List, ListItem, CheckBox, Body, Item } from "native-base";
import React, { useState } from "react";
import { List, ListItem, CheckBox, Body } from "native-base";
import React from "react";
import { Text } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { changeFilters } from "../actions";
import { RootState } from "../reducers";
import { FilterState, Genres } from "../types/filter";
import { Genres } from "../types/filter";
const genres: Array<Genres> = [
"Action",
......@@ -16,26 +16,34 @@ const genres: Array<Genres> = [
"Horror",
];
// Shows the available genres you can filter by
const FilterList = () => {
const checkedGenres: Record<Genres, boolean> = useSelector((state: RootState) => state.filtering.filter.genres);
const dispatch = useDispatch()
const checkedGenres: Record<Genres, boolean> = useSelector(
(state: RootState) => state.filtering.filter.genres
);
const dispatch = useDispatch();
const onPress = (genre: Genres) => {
const checked = checkedGenres[genre];
dispatch(changeFilters({...checkedGenres, [genre]: !checked, to: "", from: ""}));
dispatch(changeFilters({ ...checkedGenres, [genre]: !checked }));
};
const filterItems = genres.map((genre) => (
<ListItem onPress={() => onPress(genre)} key={genre}>
<CheckBox checked={checkedGenres[genre]} />
<Body>
<Text style={{paddingLeft: 10}}>{genre}</Text>
<Text style={{ paddingLeft: 10 }}>{genre}</Text>
</Body>
</ListItem>
));
return (
<>
<Text style={{fontSize: 17, paddingLeft: 15, paddingTop: 10, color: "grey"}}>Genres: </Text>
<Text
style={{ fontSize: 17, paddingLeft: 15, paddingTop: 10, color: "grey" }}
>
Genres:{" "}
</Text>
<List>{filterItems}</List>
</>
);
......
import { Header as NBHeader } from 'native-base';
import { Text, View } from 'react-native';
import React from 'react';
import SearchBar from './SearchBar';
const Header = () => {
return (
<>
<NBHeader searchBar rounded>
<SearchBar />
</NBHeader>
</>
);
};
export default Header;
import { Body, Card, H2, CardItem, View, Text } from 'native-base';
import { Image, StyleSheet } from 'react-native';
import React from 'react';
import { TouchableNativeFeedback } from 'react-native-gesture-handler';
import { Body, H2, CardItem, View } from "native-base";
import { Image, StyleSheet } from "react-native";
import React from "react";
import { TouchableNativeFeedback } from "react-native-gesture-handler";
type Props = {
id: number;
......@@ -14,9 +14,7 @@ type Props = {
const MovieCard = (props: Props) => {
return (
<TouchableNativeFeedback
onPress={props.onPress}
>
<TouchableNativeFeedback onPress={props.onPress}>
<CardItem bordered style={styles.cardItem}>
<View style={styles.imageView}>
<Image source={{ uri: props.image }} style={styles.image} />
......@@ -45,7 +43,7 @@ const styles = StyleSheet.create({
flex: 1,
width: 60,
height: 105,
resizeMode: 'contain',
resizeMode: "contain",
},
body: {
paddingTop: 20,
......
import _ from 'lodash';
import { Item, Label, Picker, Icon, Text } from 'native-base';
import React, { useState } from 'react';
import { Platform, StyleSheet } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { changeOrdering } from '../actions';
import { RootState } from '../reducers';
import { OrderingActionPayload, OrderingOptions } from '../types/ordering';
import _ from "lodash";
import { Item, Picker, Icon, Text } from "native-base";
import React from "react";
import { StyleSheet } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { changeOrdering } from "../actions";
import { RootState } from "../reducers";
import { OrderingOptions } from "../types/ordering";
const orderingOptions = [
// The available sorting options
'title - asc',
'title - desc',
'year - asc',
'year - desc',
'length - asc',
'length - desc',
"title - asc",
"title - desc",
"year - asc",
"year - desc",
"length - asc",
"length - desc",
];
// Picker for choosing what to sort the results on
......@@ -26,9 +26,9 @@ const OrderingPicker = () => {
const value = `${selectedOrdering.key} - ${selectedOrdering.order}`; // To make it equal to the orderingOptions
const onValueChange = (value: string) => {
// Converts back to object and then dispatches it
const splitValue = value.split('-');
const splitValue = value.split("-");
const key = splitValue[0].trim() as OrderingOptions;
const order = splitValue[1].trim() as 'asc' | 'desc';
const order = splitValue[1].trim() as "asc" | "desc";
dispatch(changeOrdering({ key, order }));
};
......@@ -45,11 +45,11 @@ const OrderingPicker = () => {
<Item picker style={styles.itemStyle}>
<Text style={styles.textStyle}>Order By..</Text>
<Picker
mode='dropdown'
iosIcon={<Icon name='arrow-down' />}
placeholder=''
placeholderStyle={{ color: '#bfc6ea' }}
placeholderIconColor='#007aff'
mode="dropdown"
iosIcon={<Icon name="arrow-down" />}
placeholder=""
placeholderStyle={{ color: "#bfc6ea" }}
placeholderIconColor="#007aff"
style={{ width: undefined }}
selectedValue={value}
onValueChange={onValueChange}
......@@ -62,14 +62,14 @@ const OrderingPicker = () => {
const styles = StyleSheet.create({
itemStyle: {
display: 'flex',
display: "flex",
padding: 5,
paddingLeft: 15,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: "row",