Skip to content
Snippets Groups Projects
Commit 521bc9a6 authored by Jon-Inge Heggstad's avatar Jon-Inge Heggstad
Browse files

Merge branch 'documentation-and-cleanup' into 'master'

Documentation and cleanup

Closes #11

See merge request !7
parents b93976a2 4a5f0141
No related branches found
No related tags found
1 merge request!7Documentation and cleanup
Showing
with 50 additions and 18608 deletions
### Project init
- Run `npm install` on root folder, backend folder and frontend foler.
- Run `npm install` on root folder, backend folder and frontend folder.
- To access DB run `npm start` on backend folder.
- To launch React application run `npm start` on frontend folder.
- To run tests run `npx cypress open` from frontend folder.
### Back-end
For the back-end solution to this assignment the technology architecture is as follows:
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.
- Data: PostgreSQL hosted at ElephantSQL. We chose this as it is easy to set up and maintain and fulfill the requirement of hosting our database on a virtual machine.
- Server: Node.js server using TypeScript. TypeORM is used to map the objects to their proper types. Koa is a web framework made by the team behind Express and aims to provide the similar solution to web development. Both of these dependencies were used in the design of the REST API developed for this project.
### Back-end
#### Setup
......@@ -21,13 +17,11 @@ For the back-end solution to this assignment the technology architecture is as f
The server allows a maximum of 5 concurrent connections as it's hosted on ElephantSQL's free-plan. In the case that you should not be able to connect, this is because 5 other student's are simultaneously running the server and therefore have an active connection to the database.
#### Why we went for this solution
The stack for the backend was chosen due to the relatively ease of use for the tasks in this Project. We wen with TypeORM and Koa as this allowed us to quickly set up a REST API in Node.js as the entry barrier for using these frameworks are relatively low, and the value we get from learning them are high as this allows us to make personal projects using these technologies.
#### Summary from P3
- TypeORM is an ORM that can run in multiple different platforms and allows us map our objects to the DB and handle queries to the DB. We chose this after doing some research regarding how the should map our objects, and landed on this as it seemed to be a good solution for this project. None of the team members had any experience using this, so it was a good learning experience.
- Koa is a web framework made by the team behind Express and brings a simple solution for setting up our REST API and connecting it to the DB. We decided to use Koa as the team had some experience using Express, and we therefore wanted to try something new which we had not used before.
- ElephantSQL is PostgreSQL as a service and allows us to quickly set up a PostgreSQL DB hosted by them. Cloud solutions these days are effective at delivering robust and secure solutions that often are both easier to develop for and cheaper than self-hosting. Learning about these are important and the group felt that this Project could be a good opportunity for this.
- Backend is a Node.js server.
- Frameworks used: TypeOrm and Koa.
- DB running on ElephantSQL, which is a 'PostgreSQL as a Service'
#### Documentation found at
......@@ -38,11 +32,9 @@ https://www.elephantsql.com/
Credit:
https://www.mfosullivan.com/rest-api-node-koa-postgresql/
### Front-end
The frontend was initialized using expo cli tools.
## Front-end
## 3rd party tools
The frontend was initialized using expo cli tools. It is a React Native application which can be easily tested on phones using the Expo app. You can read about expo at https://expo.io/tools.
#### Setup
......@@ -51,13 +43,49 @@ IMPORTANT! The data is fetched from the back-end, if you're gonna test this solu
- Run `npm install` inside of `projejct3/frontend`
- Run `npm start` to start the React application. This will run on `localhost:3000`
#### Why we went for this solution
### 3rd party tools
.
- Expo
### ESLint
### The application
#### Search component
The application allows users to search the database for movies using React Native Elements' SearchBar component, fitted to our use. This will return any results matching the user's input. This is default sorted to ascending based on characters, but can be set to descending depending on what the user prefers. Sorting is handled in the back-end.
#### List view
Using the FlatList component together with redux, the user can load more results simply by scrolling to the bottom of the screen. This will not work if the application is tested in the browser, as the component is designed for functionality with mobile phones. This should not be an issue as React Native application are applications designed for andriod and iOS.
#### Detailed view
At the start we wanted to set up a linter to ensure code quality. By following a tutorial it was setup with simple configurations. We could make a stricter configuration, but we just wanted to try it out and didn't have time to set more checks. To try the linter you can run these commands from the root folder:
By pressing one of the queried items, a third party component React-Navigation made for React Native will swap the focus to a nested screen containing all the information about the movie. From here, the user can click on Reviews to go even deeper into the navigation component in order to fetch, read and write reviews related to the movie viewed. At any point in time the user can click the back button to go back to the previous screen, all the way back to "Home" where the searchbar and queried list will be displayed.
#### 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.
### Technology and testing
#### Typescript
As Typescript was mandatory, the expo project was initialized with Typescript. As a result all code in the frontend folder has been written in Typescript. A linter has also been configured for Typescript to ensure that my code was robust.
#### React Native and Expo
The React Native project was initialized using the Expo CLI tools.
Some of the third party libraries I 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.
#### 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.
### ESLint
- npx eslint backend
- npx eslint frontend
......
......@@ -5,7 +5,7 @@ import { SearchBar } from "react-native-elements";
import useDebounce from "../hooks/useDebounce";
import api from "../api";
import { useAppDispatch } from "../store/store";
import { useAppDispatch } from "../store/redux/store";
import { movieSlice } from "../store/slices/movieSlice";
import { systemSlice } from "../store/slices/systemSlice";
......
import { Movie,
MOVIE_ADD,
MOVIE_CLEAR,
MOVIE_DELETE,
MovieListActionTypes,
SystemState,
UPDATE_SYSTEM,
SystemActionTypes,
MovieDisplayActionTypes,
MOVIE_SHOW,
MOVIE_FETCH,
Review,
ReviewActionTypes,
REVIEW_FETCH,
REVIEW_ADD
} from './types';
export const fetchMovie = (movies: Movie[]): MovieListActionTypes => {
return {
type: MOVIE_FETCH,
payload: movies
};
}
export const clearMovie = (): MovieListActionTypes => {
return {
type: MOVIE_CLEAR
};
}
export const addMovie = (movie: Movie): MovieListActionTypes => {
return {
type: MOVIE_ADD,
payload: movie
};
}
export const deleteMovie = (movie: Movie): MovieListActionTypes => {
return {
type: MOVIE_DELETE,
payload: movie
};
}
export const updateSystem = (system: SystemState): SystemActionTypes => {
return {
type: UPDATE_SYSTEM,
payload: system
};
}
export const showMovie = (movie: Movie): MovieDisplayActionTypes => {
return {
type: MOVIE_SHOW,
payload: movie
};
}
export const hideMovie = (): MovieDisplayActionTypes => {
return {
type: MOVIE_SHOW,
payload: {Column_1: 0, Title: "", Year: "", Released: 0, Runtime: "", Genre: "", Director: "", Writer: "", Actors: "", Plot: "", Poster: "", Metascore: 0, imdbRating: 0, rating: 0}
};
}
export const fetchReview = (reviews: Review[]): ReviewActionTypes => {
return {
type: REVIEW_FETCH,
payload: reviews
};
}
export const addReview = (review: Review): ReviewActionTypes => {
return {
type: REVIEW_ADD,
payload: review
}
}
\ No newline at end of file
// Base model for movie
export interface Movie {
Column_1: number;
Title: string;
Year: string;
Released: number;
Runtime: string;
Genre: string;
Director: string;
Writer: string;
Actors: string;
Plot: string;
Poster: string;
Metascore: number;
imdbRating: number;
rating: number;
}
// Base model for review
export interface Review {
id: string;
Column_1: number;
rating: number;
review: string;
}
// App state used for list of fetched movies
export interface MovieListState {
movieList: Movie[];
}
export const MOVIE_FETCH = 'MOVIE_FETCH';
export const MOVIE_CLEAR = 'MOVIE_CLEART';
export const MOVIE_ADD = 'MOVIE_ADD';
export const MOVIE_DELETE = 'MOVIE_DELETE';
interface fetchMovieAction {
type: typeof MOVIE_FETCH;
payload: Movie[]
}
interface clearMovieAction {
type: typeof MOVIE_CLEAR;
}
interface addMovieAction {
type: typeof MOVIE_ADD,
payload: Movie
}
interface deleteMovieAction {
type: typeof MOVIE_DELETE,
payload: Movie
}
export type MovieListActionTypes = fetchMovieAction | clearMovieAction | addMovieAction | deleteMovieAction;
// State user for pagination and other system variables
export interface SystemState {
take: number,
skip: number
}
export const UPDATE_SYSTEM = 'UPDATE_SYSTEM';
interface UpdateSystemAction {
type: typeof UPDATE_SYSTEM,
payload: SystemState
}
export type SystemActionTypes = UpdateSystemAction;
// State for showing detailed information about specific movie
export interface MovieDisplayState {
movie: Movie;
}
export const MOVIE_SHOW = 'MOVIE_SHOW';
export const MOVIE_HIDE = 'MOVIE_HIDE';
interface showMovieAction {
type: typeof MOVIE_SHOW,
payload: Movie
}
interface hideMovieAction {
type: typeof MOVIE_HIDE,
payload: Movie
}
export type MovieDisplayActionTypes = showMovieAction | hideMovieAction;
// State for showing reviews for a given movie
export interface ReviewDisplayState {
reviews: Review[]
}
export const REVIEW_FETCH = 'REVIEW_FETCH';
export const REVIEW_ADD = 'REVIEW_ADD';
interface fetchReviewAction {
type: typeof REVIEW_FETCH;
payload: Review[]
}
interface addReviewAction {
type: typeof REVIEW_ADD;
payload: Review
}
export type ReviewActionTypes = fetchReviewAction | addReviewAction;
\ No newline at end of file
import { connect, ConnectedProps } from 'react-redux';
import { RootState } from "./reducers";
import {
fetchMovie,
addMovie,
clearMovie,
showMovie,
hideMovie,
updateSystem,
fetchReview,
addReview,
} from "./actions";
const mapStateToProps = (state: RootState) => ({
movieList: state.movieList,
movie: state.movieDisplay,
system: state.system,
reviewList: state.reviews
});
export const connector = connect(mapStateToProps, {
fetchMovie,
addMovie,
clearMovie,
showMovie,
hideMovie,
updateSystem,
fetchReview,
addReview,
});
type PropsFromRedux = ConnectedProps<typeof connector>;
export type Props = PropsFromRedux;
\ No newline at end of file
import { combineReducers } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { MovieListState, MovieListActionTypes } from '../actions/types';
import {movieListReducer, movieDisplayReducer, systemReducer, reviewReducer} from './reducers';
export const rootReducer = combineReducers({
movieList: movieListReducer,
system: systemReducer,
movieDisplay: movieDisplayReducer,
reviews: reviewReducer
})
export type RootState = ReturnType<typeof rootReducer>
\ No newline at end of file
import { MovieListState,
MovieListActionTypes,
MOVIE_FETCH,
MOVIE_CLEAR,
MOVIE_ADD,
MOVIE_DELETE,
SystemState,
SystemActionTypes,
UPDATE_SYSTEM,
MovieDisplayState,
MovieDisplayActionTypes,
MOVIE_SHOW,
MOVIE_HIDE,
ReviewDisplayState,
ReviewActionTypes,
REVIEW_FETCH,
REVIEW_ADD
} from '../actions/types';
const initialMovieListState: MovieListState = {
movieList: []
}
export function movieListReducer(
state = initialMovieListState,
action: MovieListActionTypes
): MovieListState {
switch(action.type) {
case MOVIE_FETCH:
return {
movieList: action.payload
}
case MOVIE_CLEAR:
return {
movieList: []
}
case MOVIE_ADD:
return {
movieList: [...state.movieList, action.payload]
}
case MOVIE_DELETE:
return {
movieList: [...state.movieList.filter(movie => movie.Column_1 !== action.payload.Column_1)]
}
default:
return state
}
}
const initialSystemState: SystemState = {
take: 10,
skip: 0
}
export function systemReducer(
state = initialSystemState,
action: SystemActionTypes
): SystemState {
switch(action.type) {
case UPDATE_SYSTEM: {
return {
...state,
...action.payload
}
}
default:
return state
}
}
const initialMovieDisplayState: MovieDisplayState = {
movie: {Column_1: 0, Title: "", Year: "", Released: 0, Runtime: "", Genre: "", Director: "", Writer: "", Actors: "", Plot: "", Poster: "", Metascore: 0, imdbRating: 0, rating: 0}
}
export function movieDisplayReducer(
state = initialMovieDisplayState,
action: MovieDisplayActionTypes
): MovieDisplayState {
switch(action.type) {
case MOVIE_SHOW:
return {
movie: action.payload
}
case MOVIE_HIDE:
return {
movie: action.payload
}
default:
return state
}
}
const initialReviewState: ReviewDisplayState = {
reviews: []
}
export function reviewReducer(
state = initialReviewState,
action: ReviewActionTypes
): ReviewDisplayState {
switch(action.type) {
case REVIEW_FETCH:
return {
reviews: action.payload
}
case REVIEW_ADD:
return {
reviews: [...state.reviews, action.payload]
}
default:
return state
}
}
\ No newline at end of file
import React from "react";
import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
type State = {
movies: Array<{ name: string }>;
};
const slice = createSlice({
name: "test-slice",
initialState: {
movies: [],
} as State,
reducers: {
setMovies: (state: State, action: PayloadAction<State>) => ({
movies: action.payload.movies,
}),
},
});
const store = configureStore({
reducer: {
movies: slice.reducer,
},
});
export type AppState = ReturnType<typeof store.getState>;
export const useAppDispatch = () => useDispatch<typeof store.dispatch>();
const App: React.FC = () => {
return (
<Provider store={store}>
<Demo />
</Provider>
);
};
const Demo: React.FC = () => {
const dispatch = useAppDispatch();
const moviesCount = useSelector(
(state: AppState) => state.movies.movies.length
);
return (
<>
<h1>{moviesCount}</h1>
<button
onClick={() =>
dispatch(slice.actions.setMovies({ movies: [{ name: "hei" }] }))
}
>
Test
</button>
</>
);
};
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
{}
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
\ No newline at end of file
const { idText, createYield } = require("typescript");
describe("Search", () => {
it("Load landing page", () => {
cy.visit("http://localhost:3000");
cy.url().should("eq", "http://localhost:3000/");
});
it("Navigate to Search Page", () => {
cy.visit("http://localhost:3000");
cy.get("[data-cy=linkSearch]").click();
cy.url().should("eq", "http://localhost:3000/search");
});
it("Write in searchInput", () => {
cy.visit("http://localhost:3000/");
cy.get("[data-cy=linkSearch]").click();
cy.get("[data-cy=searchInput]").type("Hello, World");
cy.get("[data-cy=searchInput]").should("have.value", "Hello, World");
});
//This test should fail if backend is not running
it("Check if card list has children", () => {
cy.visit("http://localhost:3000");
cy.get("[data-cy=linkSearch]").click();
cy.get("[data-cy=cardList").children();
});
});
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
This diff is collapsed.
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.11.5",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.19.2",
"@types/reach__router": "^1.3.6",
"@types/react": "^16.9.53",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.6",
"axios": "^0.21.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-infinite-scroll-component": "^5.1.0",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.0",
"redux": "^4.0.5",
"typescript": "^4.0.5",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "eslint \"*/**/*.{ts,tsx}\" --quiet",
"lint:fix": "eslint \"*/**/*.{ts,tsx}\" --quiet --fix"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:3001",
"devDependencies": {
"cypress": "^5.5.0"
}
}
frontendjs/public/favicon.ico

3.78 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
frontendjs/public/logo192.png

5.22 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment