Skip to content
Snippets Groups Projects
Commit fa4f6569 authored by joningehe's avatar joningehe
Browse files

Final commit, everything should work well enough

- Final commit, everything should work well enough
parents 1958d149 d46f81b3
No related branches found
No related tags found
1 merge request!9Add sorting and filtration
......@@ -4,7 +4,7 @@
- To access DB run `npm start` on backend folder.
- To launch React application run `npm start` on frontend folder.
IMPORTANT! The data is fetched from the back-end, if you're gonna test this solution on an android or iOS unit using expo. You have to make sure that the backend is fetched from your computer's IP and not the default localhost. Head into `frontend/app/api/index.ts` and configure the `baseURL` to be your own. If you don't know how to find your IP, type `ipconfig` (windows), `ifconfig | grep 'inet'` (mac) or `hostname -i` (debian). Due to time constraints minimal effort went into improving the backend in this assignment as this was not specified in the requirements.
IMPORTANT! The data is fetched from the back-end, if you're gonna test this solution on an android or iOS unit using expo. You have to make sure that the backend is fetched from your computer's IP and not the default localhost. Head into `frontend/app/api/index.ts` and configure the `baseURL` to be your own. If you don't know how to find your IP, type `ipconfig` (windows), `ifconfig | grep 'inet'` (mac) or `hostname -i` (debian). Example in Windows, the adapter called `Wireless LAN Adapter Wi-Fi` is most likely going to contain your correct IPv4 or IPv6 address. If you use an IPv6 address, you might have to wrap it with `[]`. Due to time constraints minimal effort went into improving the backend in this assignment as this was not specified in the requirements.
### Back-end
......@@ -63,7 +63,7 @@ By pressing one of the queried items, a third party component React-Navigation m
#### Sorting and filtration
The user can set parameters for sorting and filtration on the Home screen before typing in the search. If these options are set after the search is performed, the user will have to search again in order to make a new query.
The user can set parameters for sorting and filtration on a modal accessed through the Home screen before typing in the search. If these options are set after the search is performed, the user will have to search again in order to make a new query.
### Technology and testing
......@@ -75,15 +75,15 @@ As Typescript was mandatory, the expo project was initialized with Typescript. A
The React Native project was initialized using the Expo CLI tools.
Some of the third party libraries I have used:
Some of the third party libraries we have used:
- Redux, with React-Redux: state management.
- React Navigation: routing for the react native application
- React Native Elements: a cross platform UI toolkit. This has mainly been used for the searchbar as I wanted a guarantee that it would work properly on both Androis and iOS. I could have used more components, but considering that most of the logic I have used are based on components in the React-Native library without heavy styling rules and advanced logic, I assumed cross-platform support would not be an issue.
- React Native Elements: a cross platform UI toolkit. This has mainly been used for the searchbar as we wanted a guarantee that it would work properly on both Androis and iOS. We could have used more components, but considering that most of the logic we have used are based on components in the React-Native library without heavy styling rules and advanced logic, we assumed cross-platform support would not be an issue.
#### End-to-end testing
The application has been heavily tested during the development. This has been done using the Expo application for Android and connecting it to the Expo service running on my computer. I can only ensure that the application will run properly on a unit rurnning Android OS as I do not own an iphone or any Apple devices to test this on.
The application has been heavily tested during the development. This has been done using the Expo application for Android and connecting it to the Expo service running on my computer. we can only ensure that the application will run properly on a unit rurnning Android OS as we do not own an iphone or any Apple devices to test this on.
### ESLint
......@@ -95,3 +95,7 @@ More information about our linter can be found at https://eslint.org/docs/user-g
#### Why we went for this solution
A linter is needed in order to ensure a robust codebase. Without it, we would be able to have poor code which would compile, but could have unforseen consequences during runtime. ESLint is configured for Typescript in this project.
### Development process
During the development of P4 both group members felt that we lacked knowledge regarding the functionality implemented by the other member. The reason for this was that during P3, one of us primarily worked on the back-end and redux, while the other handled the front-end. As a result of this, pair-programming was extensively used both while refactoring the back-end and redux, and developing the new React-Native application. Due to lack of time, and a lacking understanding of how to use redux with Typescript. The application we made in P3 had a lot of bugs and illogical handling of state. We only realized this after the fact. Refactoring the code allowed us to fix this, as well as reworking how we handled requests in the back-end. This resulted in the front-end developer getting a much better understanding of what was going on under the hood. Pair-programming on React Native also gave valuable insight to the back-end developer on how to make decent components in a React(/Native) application.
import { Context } from 'koa';
import { getManager, Repository, Like } from 'typeorm';
import { Context } from "koa";
import { getManager, Repository, Like } from "typeorm";
import { Movie } from '../models/movies';
import { Movie } from "../models/movies";
const ERROR_MSG1 = 'Movie does not exist in the database, consider adding it for future users looking for the same movie.';
const ERROR_MSG2 = 'Conflicting movie name. Consider adding identifier if distinct movies have identical names.';
const ERROR_MSG3 = 'Somethink awful has happened because the database must have gone down as it is supposed to contain movies and this code works...';
const ERROR_MSG1 =
"Movie does not exist in the database, consider adding it for future users looking for the same movie.";
const ERROR_MSG2 =
"Conflicting movie name. Consider adding identifier if distinct movies have identical names.";
const ERROR_MSG3 =
"Somethink awful has happened because the database must have gone down as it is supposed to contain movies and this code works...";
/**
* MovieController contains all the logic surrounding querying the database and performing operations on the results from the database regarding the Movie model.
*/
export default class MovieController {
public static async getMovies(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const movies: Movie[] = await movieRepository.find({
take: ctx.header.take,
skip: ctx.header.skip
skip: ctx.header.skip,
});
ctx.status = 200;
......@@ -25,15 +29,17 @@ export default class MovieController {
}
public static async getTopMovies(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const movies: Movie[] = await movieRepository.find({
order: {
Metascore: "ASC"
Metascore: "ASC",
},
take: ctx.header.take,
skip: ctx.header.skip
})
skip: ctx.header.skip,
});
if (movies) {
ctx.status = 200;
......@@ -45,7 +51,9 @@ export default class MovieController {
}
public static async getMovieId(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const movie: Movie = await movieRepository.findOne(ctx.params.Column_1);
......@@ -64,14 +72,16 @@ export default class MovieController {
// e.g. take: ctx.request.body.name.take || 10
// skip: ctx.request.body.name.skip || 0
public static async searchMovie(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const [movies, count]: any = await movieRepository.findAndCount({
where: `"Title" ILIKE '%${ctx.request.body.Title}%'`,
// order has to be either DESC or ASC
order: { Title: ctx.request.body.order || "ASC" },
take: ctx.request.body.take || 10,
skip: ctx.request.body.skip || 0
skip: ctx.request.body.skip || 0,
});
if (movies) {
......@@ -84,18 +94,23 @@ export default class MovieController {
}
public static async getMovieByTitle(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movies: Movie[] = await movieRepository.find({
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const [movies, count]: [
Movie[],
number
] = await movieRepository.findAndCount({
where: `"Title" ILIKE '%${ctx.params.title}%'`,
// order has to be either DESC or ASC
order: { Title: ctx.params.order || "ASC" },
take: ctx.params.take,
skip: Math.max(ctx.params.take-10, 0)
skip: Math.max(ctx.params.take - 10, 0),
});
if (movies) {
ctx.status = 200;
ctx.body = movies;
ctx.body = { movies: movies, count: count };
} else {
ctx.status = 400;
ctx.body = ERROR_MSG1;
......@@ -103,18 +118,23 @@ export default class MovieController {
}
public static async getMovieByTitleAndGenre(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const movies: Movie[] = await movieRepository.find({
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const [movies, count]: [
Movie[],
number
] = await movieRepository.findAndCount({
where: `"Title" ILIKE '%${ctx.params.title}%' AND "Genre" ILIKE '%${ctx.params.genre}%'`,
// order has to be either DESC or ASC
order: { Title: ctx.params.order || "ASC" },
take: ctx.params.take,
skip: Math.max(ctx.params.take-10, 0)
skip: Math.max(ctx.params.take - 10, 0),
});
if (movies) {
ctx.status = 200;
ctx.body = movies;
ctx.body = { movies: movies, count: count };
} else {
ctx.status = 400;
ctx.body = ERROR_MSG1;
......@@ -125,17 +145,22 @@ export default class MovieController {
// Expected body has categories
// Take and skip for pagination
public static async searchMoviesByGenre(ctx: Context) {
const movieRepository: Repository<Movie> = getManager().getRepository(Movie);
const [movies, count]: [Movie[], number] = await movieRepository.findAndCount({
const movieRepository: Repository<Movie> = getManager().getRepository(
Movie
);
const [movies, count]: [
Movie[],
number
] = await movieRepository.findAndCount({
where: `"Genre" ILIKE '%${ctx.request.body.Genre}%'`,
// order has to be either DESC or ASC
order: {
Title: ctx.request.body.order
Title: ctx.request.body.order,
},
take: ctx.request.body.take || 10,
skip: ctx.request.body.skip || 0
})
skip: ctx.request.body.skip || 0,
});
if (movies) {
ctx.status = 200;
......
......@@ -10,6 +10,7 @@ import Home from "./app/views/Home.component";
import { MovieDetail } from "./app/views/MovieDetail.component";
import { ReviewList } from "./app/views/ReviewList.component";
// We use the navigator component to allow for easy navigation inside of our application.
const Stack = createStackNavigator<StackParamList>();
const App: React.FC = (): JSX.Element => {
......
......@@ -2,5 +2,5 @@ import axios from "axios";
// You will need to set this to your ip address when running. This is the only variable that has to be changed.
export default axios.create({
baseURL: "http://[2001:700:300:4100:597c:4fc9:563a:9e28]:3001",
baseURL: "http://[THIS_IS_WHERE_YOUR_IP_GOES]:3001",
});
......@@ -4,6 +4,13 @@ import { movieSlice } from "../slices/movieSlice";
import { reviewSlice } from "../slices/reviewSlice";
import { systemSlice } from "../slices/systemSlice";
/**
* store contains the redux configuration and is a combination of all the states, actions and reducers made in slices.
* Access to following:
* @constructs movieSlice
* @constructs reviewSlice
* @constructs systemSlice
*/
export const store = configureStore({
reducer: {
movies: movieSlice.reducer,
......@@ -12,6 +19,8 @@ export const store = configureStore({
},
});
// We need to create a type for typescript.
export type AppState = ReturnType<typeof store.getState>;
// We export the dispatching of our store in order to effectively use it in our components.
export const useAppDispatch = () => useDispatch<typeof store.dispatch>();
......@@ -18,11 +18,13 @@ export interface Movie {
rating: number;
}
// Type of the state found in the store
type MovieState = {
searchTerm: string;
movies: Array<Movie>;
};
// Our slices, this is essentially a combination of our state, reducers and actions found in our store.
export const movieSlice = createSlice({
name: "movie-slice",
initialState: {
......
......@@ -8,10 +8,12 @@ export interface Review {
review: string;
}
// Type of the state found in the store
type ReviewState = {
reviews: Array<Review>;
};
// Our slices, this is essentially a combination of our state, reducers and actions found in our store.
export const reviewSlice = createSlice({
name: "review-slice",
initialState: {
......
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
// State user for pagination and other system variables
// Base model for out system state
export interface System {
count: number;
}
// Type of the state found in the store
type SystemState = {
count: number;
};
// Our slices, this is essentially a combination of our state, reducers and actions found in our store.
export const systemSlice = createSlice({
name: "system-slice",
initialState: {
......
import React, { useEffect, useState } from "react";
import { View, FlatList, StyleSheet, Picker } from "react-native";
import { View, FlatList, StyleSheet, Picker, Text } from "react-native";
import axios from "axios";
import { AppState, useAppDispatch } from "../store/redux/store";
import { SearchHandler } from "../components/SearchHandler.component";
import { useSelector } from "react-redux";
import { ListItem } from "react-native-elements";
import api from "../api";
import { Movie, movieSlice } from "../store/slices/movieSlice";
import { systemSlice } from "../store/slices/systemSlice";
import { MovieListItem } from "./MovieListItem.component";
import { sys } from "typescript";
import { systemSlice } from "../store/slices/systemSlice";
const Home: React.FC = (): JSX.Element => {
const dispatch = useAppDispatch();
......@@ -21,7 +19,8 @@ const Home: React.FC = (): JSX.Element => {
const [sortValue, setSortValue] = useState("ASC");
const [filterValue, setFilterValue] = useState("");
// This does not work, dispatch is async, so we will have to rework pagination. The list works.
// As it stands, this simply clear the movie page, implemented in case anything gets bugged.
// The intended use of this function doesn't really make sense in a movie DB
const refreshMovies = () => {
setRefreshing(true);
dispatch(movieSlice.actions.clearMovies());
......@@ -29,10 +28,12 @@ const Home: React.FC = (): JSX.Element => {
};
const fetchMoreMovies = async () => {
// Not fetch more movies than in DB
if (movies.length >= count) {
return;
}
// We create the endpoint based on whether a genre has been set or not, these are seperate in the back-end
const endpoint =
filterValue == ""
? `${api.defaults.baseURL}/movies/${searchTerm}/${
......@@ -45,15 +46,18 @@ const Home: React.FC = (): JSX.Element => {
axios
.get(endpoint)
.then((response) => {
response.data.map((movie: Movie) => {
response.data.movies.map((movie: Movie) => {
if (!movies.includes(movie)) {
dispatch(movieSlice.actions.addMovie(movie));
}
dispatch(systemSlice.actions.updateSystem(response.data.count));
});
})
.catch((e) => console.log(e));
};
// We need to clear the list and fetch new ones when genre or sorting has been changed
useEffect(() => {
dispatch(movieSlice.actions.clearMovies());
fetchMoreMovies();
......@@ -92,13 +96,17 @@ const Home: React.FC = (): JSX.Element => {
<FlatList
data={movies}
renderItem={({ item }) => <MovieListItem {...item} />}
keyExtractor={(item) => item.Column_1.toString()}
keyExtractor={(item, index) =>
Math.floor(Math.random() * 100).toString() +
item.Column_1.toString()
}
ItemSeparatorComponent={() => <View></View>}
refreshing={refreshing}
onRefresh={refreshMovies}
onEndReached={fetchMoreMovies}
onMomentumScrollEnd={() => fetchMoreMovies()}
/>
</View>
<Text>You either haven't searched or there are no more movies.</Text>
</View>
);
};
......@@ -106,13 +114,8 @@ const Home: React.FC = (): JSX.Element => {
const styles = StyleSheet.create({
container: {
backgroundColor: "#eee",
flex: 1,
},
});
export default Home;
/*
Missing support:
<Picker.Item label="Top-bottom rated" value="top" />
<Picker.Item label="Bottom-top rated" value="bot" />
*/
......@@ -22,8 +22,10 @@ const MovieDetail = (): JSX.Element => {
const navigation = useNavigation<ReviewScreenNavigationProp>();
const dispatch = useAppDispatch();
// We get the movie from the navigation component when a user clicks on it
const { movie } = route.params;
// Fetch all the reviews for the movie
const showReviews = async () => {
axios
.get(`${api.defaults.baseURL}/reviews/${movie.Column_1}`)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment