diff --git a/FE/angular/src/app/app-routing.module.ts b/FE/angular/src/app/app-routing.module.ts index ff15850..9dbd846 100644 --- a/FE/angular/src/app/app-routing.module.ts +++ b/FE/angular/src/app/app-routing.module.ts @@ -1,17 +1,12 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { MovieComponent } from './components/movie/movie.component'; -import { MoviesComponent } from './components/movies/movies.component'; const routes: Routes = [ { path: '', - component: MoviesComponent + loadChildren: () => import('./components/movies/movies.module').then((m) => m.MoviesModule) }, - { - path: 'movie/:id', - component: MovieComponent - } + { path: 'movie/:id', loadChildren: () => import('./components/movie/movie.module').then((m) => m.MovieModule) } ]; @NgModule({ diff --git a/FE/angular/src/app/app.component.html b/FE/angular/src/app/app.component.html index c605841..3e12256 100644 --- a/FE/angular/src/app/app.component.html +++ b/FE/angular/src/app/app.component.html @@ -1,6 +1,5 @@
-

{{ pageTitle }}

diff --git a/FE/angular/src/app/app.component.ts b/FE/angular/src/app/app.component.ts index 1f91d1c..6c161d4 100644 --- a/FE/angular/src/app/app.component.ts +++ b/FE/angular/src/app/app.component.ts @@ -4,6 +4,6 @@ import { Component } from '@angular/core'; selector: 'app-root', templateUrl: './app.component.html' }) -export class AppComponent { - public pageTitle = 'Movies'; +export class AppComponent{ + } diff --git a/FE/angular/src/app/app.module.ts b/FE/angular/src/app/app.module.ts index 443f97a..c08f1d8 100644 --- a/FE/angular/src/app/app.module.ts +++ b/FE/angular/src/app/app.module.ts @@ -3,29 +3,14 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { MovieComponent } from './components/movie/movie.component'; -import { MoviesComponent } from './components/movies/movies.component'; -import { DecadesComponent } from './components/navigation/decades/decades.component'; -import { GoBackComponent } from './components/navigation/go-back/go-back.component'; -import { GoDetailsComponent } from './components/navigation/go-details/go-details.component'; -import { GoImdbComponent } from './components/navigation/go-imdb/go-imdb.component'; import { NavigationService } from './components/navigation/navigation.service'; -import { SidebarComponent } from './components/sidebar/sidebar.component'; -import { DataService } from './services/data.service'; +import { MovieModule } from './components/movie/movie.module'; +import { MoviesModule } from './components/movies/movies.module'; @NgModule({ - declarations: [ - MoviesComponent, - MovieComponent, - SidebarComponent, - GoBackComponent, - GoDetailsComponent, - GoImdbComponent, - AppComponent, - DecadesComponent - ], - imports: [BrowserModule, AppRoutingModule, HttpClientModule], - providers: [DataService, NavigationService], + declarations: [AppComponent], + imports: [BrowserModule, AppRoutingModule, HttpClientModule, MovieModule, MoviesModule], + providers: [NavigationService], bootstrap: [AppComponent] }) export class AppModule {} diff --git a/FE/angular/src/app/components/movie-detail/movie-detail.component.html b/FE/angular/src/app/components/movie-detail/movie-detail.component.html new file mode 100644 index 0000000..a22230d --- /dev/null +++ b/FE/angular/src/app/components/movie-detail/movie-detail.component.html @@ -0,0 +1,35 @@ +
+
+
+
{{ movie.Title }} {{movie.Year}}
+ + +
+ {{ movie.Plot }} +
+ +
+
\ No newline at end of file diff --git a/FE/angular/src/app/components/movie-detail/movie-detail.component.scss b/FE/angular/src/app/components/movie-detail/movie-detail.component.scss new file mode 100644 index 0000000..debce8b --- /dev/null +++ b/FE/angular/src/app/components/movie-detail/movie-detail.component.scss @@ -0,0 +1,3 @@ +.movie-detail { + display: flex; +} \ No newline at end of file diff --git a/FE/angular/src/app/components/movie-detail/movie-detail.component.ts b/FE/angular/src/app/components/movie-detail/movie-detail.component.ts new file mode 100644 index 0000000..2d2af69 --- /dev/null +++ b/FE/angular/src/app/components/movie-detail/movie-detail.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { MovieComplete } from '../movie/movie.models'; + +@Component({ + selector: 'movie-detail', + templateUrl: './movie-detail.component.html', + styleUrls: ['./movie-detail.component.scss'] +}) +export class MovieDetailComponent { + @Input() isMovies = false; + + @Input() movie: MovieComplete; + + public fallBackImage: string = './assets/images/placeholder.jpg'; + + handleImageError(event: Event): void { + (event.target as HTMLImageElement).src = this.fallBackImage + } +} diff --git a/FE/angular/src/app/components/movie-detail/movie-detail.module.ts b/FE/angular/src/app/components/movie-detail/movie-detail.module.ts new file mode 100644 index 0000000..94ab081 --- /dev/null +++ b/FE/angular/src/app/components/movie-detail/movie-detail.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MovieDetailComponent } from './movie-detail.component'; +import { GoImdbModule } from '../navigation/go-imdb/go-imdb.module'; +import { GoDetailsModule } from '../navigation/go-details/go-details.module'; + +@NgModule({ + declarations: [MovieDetailComponent], + imports: [CommonModule, GoImdbModule, GoDetailsModule], + exports: [MovieDetailComponent] +}) +export class MovieDetailModule {} diff --git a/FE/angular/src/app/components/movie/data.service.spec.ts b/FE/angular/src/app/components/movie/data.service.spec.ts new file mode 100644 index 0000000..a670670 --- /dev/null +++ b/FE/angular/src/app/components/movie/data.service.spec.ts @@ -0,0 +1,37 @@ +import { HttpClient } from '@angular/common/http'; +import { mockProvider, SpectatorService } from '@ngneat/spectator'; +import { createServiceFactory } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; +import { MovieDataService } from './data.service'; +import { mockMovies } from '../../../app/tests/mock-data'; +import { Constants } from '../../../app/constants/constant'; + +const mockGet = jest.fn().mockReturnValue(of([])); +const mockHttpClient = mockProvider(HttpClient, { + get: mockGet +}); + +describe('MovieDataService', () => { + let spectator: SpectatorService; + let service: MovieDataService; + const createService = createServiceFactory({ + service: MovieDataService, + imports: [], + declarations: [], + providers: [mockHttpClient] + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + spectator = createService(); + service = spectator.service; + }); + + test('http method should return the desired result', () => { + expect(service).toBeTruthy(); + mockGet.mockReturnValueOnce(of(mockMovies[1])); + service.getMovie(mockMovies[1].imdbID); + expect(mockGet).toBeCalledWith(`${Constants.serviceUrl}i=${mockMovies[1].imdbID}`); + }); +}); diff --git a/FE/angular/src/app/components/movie/data.service.ts b/FE/angular/src/app/components/movie/data.service.ts new file mode 100644 index 0000000..14060ed --- /dev/null +++ b/FE/angular/src/app/components/movie/data.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { Observable, map } from 'rxjs'; +import { Constants } from '../../constants/constant'; +import { HttpClient } from '@angular/common/http'; +import { MovieComplete, MovieDetails } from './movie.models'; + +@Injectable() +export class MovieDataService { + constructor(private http: HttpClient) {} + + public getMovie(id: string): Observable { + return this.http.get(`${Constants.serviceUrl}i=${id}`).pipe( + map(({ Actors, Director, Genre, imdbID, Plot, Poster, Rated, Released, Runtime, Title, Type, Writer, Year }) => ({ + Actors, + Director, + Genre, + imdbID, + Plot, + Poster: Poster.replace(Constants.posterUrl, Constants.replacePosterUrl), + Rated, + Released, + Runtime, + Title, + Type, + Writer, + Year: parseInt(Year as string) + })) + ); + } +} diff --git a/FE/angular/src/app/components/movie/movie-routing.module.ts b/FE/angular/src/app/components/movie/movie-routing.module.ts new file mode 100644 index 0000000..9e0b0df --- /dev/null +++ b/FE/angular/src/app/components/movie/movie-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MovieComponent } from './movie.component'; + +const routes: Routes = [{ path: '', component: MovieComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MovieRoutingModule {} diff --git a/FE/angular/src/app/components/movie/movie.component.html b/FE/angular/src/app/components/movie/movie.component.html index f9078bd..3bdf839 100644 --- a/FE/angular/src/app/components/movie/movie.component.html +++ b/FE/angular/src/app/components/movie/movie.component.html @@ -1,38 +1,7 @@ -
    +
    • -
      -
      -
      {{ movie.Title }} ({{ movie.Year }})
      - -
        -
      • - Director: - {{ movie.Director }} -
      • -
      • - Writer: - {{ movie.Writer }} -
      • -
      • - Stars: - {{ movie.Actors }} -
      • -
      -
      - {{ movie.Plot }} -
      - -
      +
    diff --git a/FE/angular/src/app/components/movie/movie.component.spec.ts b/FE/angular/src/app/components/movie/movie.component.spec.ts index a47c7d9..f4b7bd1 100644 --- a/FE/angular/src/app/components/movie/movie.component.spec.ts +++ b/FE/angular/src/app/components/movie/movie.component.spec.ts @@ -1,14 +1,16 @@ import { ActivatedRoute } from '@angular/router'; import { mockProvider, Spectator } from '@ngneat/spectator'; import { createComponentFactory } from '@ngneat/spectator/jest'; -import { DataService } from '../../services/data.service'; +import { MovieDataService } from './data.service'; import { MovieComponent } from './movie.component'; +import { of } from 'rxjs'; +import { mockMovies } from '../../tests/mock-data'; const mockActivatedRoute = mockProvider(ActivatedRoute, { - params: jest.fn() + params: of(({id: 'tt123'})) }); -const mockDataService = mockProvider(DataService, { - getMovie: jest.fn() +const mockDataService = mockProvider(MovieDataService, { + getMovie: jest.fn().mockReturnValue(of({ ...mockMovies[0] })) }); describe('MovieComponent', () => { @@ -26,10 +28,18 @@ describe('MovieComponent', () => { beforeEach(() => { spectator = createComponent(); component = spectator.component; + component.ngOnInit(); }); test('should create the component', () => { - component.ngOnInit(); expect(component).toBeTruthy(); }); + + test('should have the expected movie id', () => { + expect(component.movieId).toBe('tt123') + }) + + test('should not have the following movie id', () => { + expect(component.movieId).not.toBe('tt124') + }) }); diff --git a/FE/angular/src/app/components/movie/movie.component.ts b/FE/angular/src/app/components/movie/movie.component.ts index de80c47..877c89d 100644 --- a/FE/angular/src/app/components/movie/movie.component.ts +++ b/FE/angular/src/app/components/movie/movie.component.ts @@ -1,25 +1,34 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { tap } from 'rxjs'; -import { DataService, MovieComplete } from '../../services/data.service'; +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { Observable, map, switchMap, tap } from 'rxjs'; +import { MovieDataService } from './data.service'; +import { MovieComplete } from './movie.models'; +import { Title } from '@angular/platform-browser'; +import { Constants } from '../../../app/constants/constant'; @Component({ selector: 'app-movie', templateUrl: './movie.component.html' }) -export class MovieComponent implements OnDestroy, OnInit { +export class MovieComponent implements OnInit { public movie: MovieComplete; public movieId = ''; - private movieSubscription: any; - constructor(private activatedRoute: ActivatedRoute, private dataService: DataService) {} + /* Replaced the subscription and handling it as observable with async pipe */ + public movie$: Observable; + + constructor( + private activatedRoute: ActivatedRoute, + private movieDataService: MovieDataService, + private title: Title + ) {} public ngOnInit() { - this.activatedRoute.params.pipe(tap(({ id }) => (this.movieId = id))); - this.movieSubscription = this.dataService.getMovie(this.movieId).pipe(tap((data) => (this.movie = data))); - } + this.title.setTitle(Constants.movieDetailPage); - public ngOnDestroy(): void { - this.movieSubscription.unsubscribe(); + this.movie$ = this.activatedRoute.params.pipe( + map((params: Params) => params.id), + switchMap((movieId: string) => this.movieDataService.getMovie(movieId)) + ); } } diff --git a/FE/angular/src/app/components/movie/movie.models.ts b/FE/angular/src/app/components/movie/movie.models.ts new file mode 100644 index 0000000..566f4db --- /dev/null +++ b/FE/angular/src/app/components/movie/movie.models.ts @@ -0,0 +1,26 @@ +export interface Movie { + imdbID: string; + Poster: string; + Title: string; + Type: string; + Year: string | number; +} + +export interface MovieComplete extends MovieDetails { + Year: number; +} + +export interface MovieDetails extends Movie { + Actors: string; + Director: string; + Genre: string; + Plot: string; + Rated: string; + Released: string; + Runtime: string; + Writer: string; +} + +export interface MovieComplete extends MovieDetails { + Year: number; +} diff --git a/FE/angular/src/app/components/movie/movie.module.ts b/FE/angular/src/app/components/movie/movie.module.ts new file mode 100644 index 0000000..eabd257 --- /dev/null +++ b/FE/angular/src/app/components/movie/movie.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MovieRoutingModule } from './movie-routing.module'; +import { MovieComponent } from './movie.component'; +import { GoBackModule } from '../navigation/go-back/go-back.module'; +import { GoImdbModule } from '../navigation/go-imdb/go-imdb.module'; +import { MovieDataService } from './data.service'; +import { MovieDetailModule } from '../movie-detail/movie-detail.module'; + +@NgModule({ + declarations: [MovieComponent], + imports: [CommonModule, MovieRoutingModule, GoBackModule, GoImdbModule, MovieDetailModule], + providers: [MovieDataService] +}) +export class MovieModule {} diff --git a/FE/angular/src/app/components/movies/data.service.spec.ts b/FE/angular/src/app/components/movies/data.service.spec.ts new file mode 100644 index 0000000..02013e7 --- /dev/null +++ b/FE/angular/src/app/components/movies/data.service.spec.ts @@ -0,0 +1,44 @@ +import { SpectatorService, createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; +import { of } from 'rxjs'; +import { MoviesDataService } from './data.service'; +import { HttpClient } from '@angular/common/http'; +import { mockMovies } from '../../../app/tests/mock-data'; +import { Constants } from '../../../app/constants/constant'; +import { MovieDataService } from '../movie/data.service'; + +const mockGet = jest.fn().mockReturnValue(of([])); +const mockHttpClient = mockProvider(HttpClient, { + get: mockGet +}); + +describe('MoviesDataService', () => { + let spectator: SpectatorService; + let service: MoviesDataService; + const createService = createServiceFactory({ + service: MoviesDataService, + imports: [], + declarations: [], + providers: [mockHttpClient, MovieDataService] + }); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + spectator = createService(); + service = spectator.service; + }); + test('should create the service', () => { + expect(service).toBeTruthy(); + }); + test('should return all movies', () => { + expect(service.getFilteredMovies(mockMovies)).toEqual(mockMovies); + }); + test('should return only movies from that decade', () => { + expect(service.getFilteredMovies(mockMovies, 2010)).toEqual([mockMovies[1]]); + }); + test('should call get method', () => { + mockGet.mockReturnValueOnce(of({ Response: 'True', Search: mockMovies, totalResults: '2' })); + mockGet.mockReturnValue(of(mockMovies[1])); + service.getMovies(); + expect(mockGet).toBeCalledWith(`${Constants.serviceUrl}s=Batman&type=movie`); + }); +}); diff --git a/FE/angular/src/app/components/movies/data.service.ts b/FE/angular/src/app/components/movies/data.service.ts new file mode 100644 index 0000000..de93424 --- /dev/null +++ b/FE/angular/src/app/components/movies/data.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { forkJoin, Observable, of } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; +import { MovieDataService } from '../movie/data.service'; +import { MovieComplete } from '../movie/movie.models'; +import { MovieData, SearchResults } from './movies.models'; +import { Constants } from '../../constants/constant'; +import { HttpClient } from '@angular/common/http'; + +@Injectable() +export class MoviesDataService { + private storedMovies: MovieData = { Search: [], Decades: [] }; + private decades: number[] = []; + + constructor(private http: HttpClient, private movieDataAccess: MovieDataService) {} + + public getFilteredMovies(movies: MovieComplete[], decade?: number): MovieComplete[] { + if (!decade) { + return movies; + } + + const decadeLimit = decade + 10; + return movies.filter((movie) => movie.Year >= decade && movie.Year < decadeLimit); + } + + public getMovies(): Observable { + if (this.storedMovies && this.storedMovies.Search.length) { + return of(this.storedMovies); + } + + return this.http.get(`${Constants.serviceUrl}s=Batman&type=movie`).pipe( + mergeMap(({ Search }) => + forkJoin( + Search.map(({ imdbID, Year }) => { + // add decade to decades + const decade = Math.ceil(parseInt(Year as string) / 10) * 10 - 10; + if (this.decades.indexOf(decade) < 0) { + this.decades.push(decade); + } + return this.movieDataAccess.getMovie(imdbID); + }) + ) + ), + map((Search) => { + Search = Search.sort(({ Year: year1 }: MovieComplete, { Year: year2 }: MovieComplete) => year1 - year2); + this.decades.sort((a, b) => a - b); + this.storedMovies = { Search, Decades: this.decades }; + + return this.storedMovies; + }) + ); + } +} diff --git a/FE/angular/src/app/components/movies/movies-routing.module.ts b/FE/angular/src/app/components/movies/movies-routing.module.ts new file mode 100644 index 0000000..90edebb --- /dev/null +++ b/FE/angular/src/app/components/movies/movies-routing.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MoviesComponent } from './movies.component'; + +const routes: Routes = [{ path: '', component: MoviesComponent }]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class MovieRoutingModule {} diff --git a/FE/angular/src/app/components/movies/movies.component.html b/FE/angular/src/app/components/movies/movies.component.html index 5429607..637dbae 100644 --- a/FE/angular/src/app/components/movies/movies.component.html +++ b/FE/angular/src/app/components/movies/movies.component.html @@ -1,28 +1,14 @@ - - -
      -
    • -
      -
      -
      {{ movie.Title }}
      - -
      - {{ movie.Plot }} -
      - -
      -
    • -
    +
    + + +
      +
    • + +
    • +
    +
    diff --git a/FE/angular/src/app/components/movies/movies.component.spec.ts b/FE/angular/src/app/components/movies/movies.component.spec.ts index 719f1c7..5d79e4f 100644 --- a/FE/angular/src/app/components/movies/movies.component.spec.ts +++ b/FE/angular/src/app/components/movies/movies.component.spec.ts @@ -1,113 +1,58 @@ import { mockProvider, Spectator } from '@ngneat/spectator'; import { createComponentFactory } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; -import { DataService } from '../../services/data.service'; +import { MoviesDataService } from './data.service'; import { MoviesComponent } from './movies.component'; - -const mockDecades = [2000]; -const mockMovies = [ - { - Title: 'Mock Movie', - Year: 2000, - Rated: 'G', - Released: '01 Jan 2000', - Runtime: '90 min', - Genre: 'Mock Genre', - Director: 'Director McMock', - Writer: 'Writer Mock, Writer Mockerson', - Actors: 'Actor McMock, Actor Mockerson', - Plot: 'Mock movie plot summary.', - Poster: - 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', - imdbID: 'tt123', - Type: 'movie' - }, - { - Title: 'Mock Movie 2', - Year: 2011, - Rated: 'G', - Released: '01 Jan 2011', - Runtime: '90 min', - Genre: 'Mock Genre', - Director: 'Director McMock', - Writer: 'Writer Mock, Writer Mockerson', - Actors: 'Actor McMock, Actor Mockerson', - Plot: 'Mock movie plot summary.', - Poster: - 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', - imdbID: 'tt123', - Type: 'movie' - } -]; +import { mockDecades, mockMovies } from '../../tests/mock-data'; const mockGetMovies = jest.fn().mockReturnValue(of({ Decades: mockDecades, Search: mockMovies })); const mockGetFilteredMovies = jest.fn().mockReturnValue([mockMovies[0]]); -const mockDataService = mockProvider(DataService, { +const mockDataService = mockProvider(MoviesDataService, { getMovies: mockGetMovies, getFilteredMovies: mockGetFilteredMovies }); -describe('MovieComponent', () => { +describe('MoviesComponent', () => { let spectator: Spectator; let component: MoviesComponent; const createComponent = createComponentFactory({ component: MoviesComponent, - imports: [], - declarations: [], providers: [mockDataService], - shallow: true, - detectChanges: false + shallow: true }); beforeEach(() => { spectator = createComponent(); component = spectator.component; + component.ngOnInit(); }); test('should create the component', () => { - component.ngOnInit(); expect(component).toBeTruthy(); }); - describe('ngOnInit', () => { - beforeEach(() => { - component.ngOnInit(); - }); - test('should set decades', () => { - expect(component.decades).toEqual(mockDecades); - }); - test('should set movies array', () => { - expect(component.movies).toEqual(mockMovies); - }); + test('should set decades', () => { + expect(component.decades).toEqual(mockDecades); + }); + + test('should set movies array', () => { + expect(component.movies).toEqual(mockMovies); + }); + + test('should set filteredMovies', () => { + component.displayMovies(); + expect(component.filteredMovies).toEqual([mockMovies[0]]); + }); + + test('should set currDecade', () => { + component.displayMovies(2000); + expect(component.currDecade).toEqual(2000); }); - describe('displayMovies', () => { - beforeEach(() => { - component.ngOnInit(); - }); - describe('WHEN movies are defined', () => { - beforeEach(() => { - component.displayMovies(); - }); - test('should set filteredMovies', () => { - expect(component.filteredMovies).toEqual([mockMovies[0]]); - }); - describe('AND a decade is passed in', () => { - beforeEach(() => { - component.displayMovies(2000); - }); - test('should set currDecade', () => { - expect(component.currDecade).toEqual(2000); - }); - }); - }); - describe('WHEN movies are undefined', () => { - test('should set filteredMovies to an empty array', () => { - component.movies = []; - spectator.detectComponentChanges(); - component.displayMovies(); - expect(component.filteredMovies).toEqual([]); - }); - }); + test('should set filteredMovies to an empty array', () => { + component.movies = []; + spectator.detectComponentChanges(); + component.displayMovies(); + expect(component.filteredMovies).toEqual([]); }); }); diff --git a/FE/angular/src/app/components/movies/movies.component.ts b/FE/angular/src/app/components/movies/movies.component.ts index d8203d2..b87c231 100644 --- a/FE/angular/src/app/components/movies/movies.component.ts +++ b/FE/angular/src/app/components/movies/movies.component.ts @@ -1,22 +1,28 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { tap } from 'rxjs'; -import { DataService, MovieComplete } from '../../services/data.service'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { MoviesDataService } from './data.service'; +import { MovieComplete } from '../movie/movie.models'; +import { Title } from '@angular/platform-browser'; +import { Constants } from '../../../app/constants/constant'; +import { MovieData } from './movies.models'; @Component({ selector: 'app-movies', templateUrl: './movies.component.html' }) -export class MoviesComponent implements OnDestroy, OnInit { +export class MoviesComponent implements OnInit { public currDecade: number | undefined; public decades: number[] = []; + public fetchMoviesData$: Observable | undefined; public filteredMovies: MovieComplete[] = []; public movies: MovieComplete[] = []; - private moviesSubscription: any; - constructor(private dataService: DataService) {} + constructor(private dataService: MoviesDataService, private title: Title) {} - public ngOnInit(): void { - this.moviesSubscription = this.dataService.getMovies().pipe( + ngOnInit(): void { + this.title.setTitle(Constants.moviesHomePage); + this.fetchMoviesData$ = this.dataService.getMovies().pipe( tap((data) => { this.decades = data.Decades; this.movies = data.Search; @@ -25,16 +31,7 @@ export class MoviesComponent implements OnDestroy, OnInit { ); } - public ngOnDestroy(): void { - this.moviesSubscription.unsubscribe(); - } - - public displayMovies(decade?: number): void { - if (!this.movies?.length) { - this.filteredMovies = []; - return; - } - + displayMovies(decade?: number): void { this.currDecade = decade; this.filteredMovies = this.dataService.getFilteredMovies(this.movies, decade); } diff --git a/FE/angular/src/app/components/movies/movies.models.ts b/FE/angular/src/app/components/movies/movies.models.ts new file mode 100644 index 0000000..0fbaff3 --- /dev/null +++ b/FE/angular/src/app/components/movies/movies.models.ts @@ -0,0 +1,12 @@ +import { Movie, MovieComplete } from '../movie/movie.models'; + +export interface SearchResults { + Response: string; + Search: Movie[]; + totalResults: string; +} + +export interface MovieData { + Decades: number[]; + Search: MovieComplete[]; +} diff --git a/FE/angular/src/app/components/movies/movies.module.ts b/FE/angular/src/app/components/movies/movies.module.ts new file mode 100644 index 0000000..c7302ab --- /dev/null +++ b/FE/angular/src/app/components/movies/movies.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MovieRoutingModule } from './movies-routing.module'; +import { MoviesComponent } from './movies.component'; +import { GoDetailsModule } from '../navigation/go-details/go-details.module'; +import { DecadesModule } from '../navigation/decades/decades.module'; +import { MovieDataService } from '../movie/data.service'; +import { MoviesDataService } from './data.service'; +import { MovieDetailModule } from '../movie-detail/movie-detail.module'; + +@NgModule({ + declarations: [MoviesComponent], + imports: [CommonModule, MovieRoutingModule, GoDetailsModule, DecadesModule, MovieDetailModule], + providers: [MovieDataService, MoviesDataService] +}) +export class MoviesModule {} diff --git a/FE/angular/src/app/components/navigation/decades/decades.component.html b/FE/angular/src/app/components/navigation/decades/decades.component.html index df686c4..cde55f5 100644 --- a/FE/angular/src/app/components/navigation/decades/decades.component.html +++ b/FE/angular/src/app/components/navigation/decades/decades.component.html @@ -1 +1 @@ - + diff --git a/FE/angular/src/app/components/navigation/decades/decades.component.scss b/FE/angular/src/app/components/navigation/decades/decades.component.scss new file mode 100644 index 0000000..7e419c1 --- /dev/null +++ b/FE/angular/src/app/components/navigation/decades/decades.component.scss @@ -0,0 +1,4 @@ +.center { + display: flex; + justify-content: center; +} \ No newline at end of file diff --git a/FE/angular/src/app/components/navigation/decades/decades.component.ts b/FE/angular/src/app/components/navigation/decades/decades.component.ts index 7fa6bc1..62b9f8f 100644 --- a/FE/angular/src/app/components/navigation/decades/decades.component.ts +++ b/FE/angular/src/app/components/navigation/decades/decades.component.ts @@ -5,7 +5,8 @@ import { BaseLink, Link } from '../../sidebar/sidebar.component'; @Component({ selector: 'app-decades', - templateUrl: './decades.component.html' + templateUrl: './decades.component.html', + styleUrls: ['./decades.component.scss'] }) export class DecadesComponent implements OnInit { @Input() public currDecade: number | undefined; diff --git a/FE/angular/src/app/components/navigation/decades/decades.module.ts b/FE/angular/src/app/components/navigation/decades/decades.module.ts new file mode 100644 index 0000000..294e600 --- /dev/null +++ b/FE/angular/src/app/components/navigation/decades/decades.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { DecadesComponent } from './decades.component'; +import { SidebarModule } from '../../sidebar/sidebar.module'; + +@NgModule({ + declarations: [DecadesComponent], + imports: [CommonModule, SidebarModule], + exports: [DecadesComponent] +}) +export class DecadesModule {} diff --git a/FE/angular/src/app/components/navigation/go-back/go-back.module.ts b/FE/angular/src/app/components/navigation/go-back/go-back.module.ts new file mode 100644 index 0000000..ea28c83 --- /dev/null +++ b/FE/angular/src/app/components/navigation/go-back/go-back.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { GoBackComponent } from './go-back.component'; +import { SidebarModule } from '../../sidebar/sidebar.module'; + +@NgModule({ + declarations: [GoBackComponent], + imports: [CommonModule, SidebarModule], + exports: [GoBackComponent] +}) +export class GoBackModule {} diff --git a/FE/angular/src/app/components/navigation/go-details/go-details.component.html b/FE/angular/src/app/components/navigation/go-details/go-details.component.html index 92d5deb..12c711a 100644 --- a/FE/angular/src/app/components/navigation/go-details/go-details.component.html +++ b/FE/angular/src/app/components/navigation/go-details/go-details.component.html @@ -1 +1 @@ - + diff --git a/FE/angular/src/app/components/navigation/go-details/go-details.component.ts b/FE/angular/src/app/components/navigation/go-details/go-details.component.ts index 96b4b78..ba0e584 100644 --- a/FE/angular/src/app/components/navigation/go-details/go-details.component.ts +++ b/FE/angular/src/app/components/navigation/go-details/go-details.component.ts @@ -7,6 +7,7 @@ import { NavigationService } from '../navigation.service'; }) export class GoDetailsComponent { @Input() public imdbId: string; + @Input() public movieName: string; constructor(private navigationService: NavigationService) {} diff --git a/FE/angular/src/app/components/navigation/go-details/go-details.module.ts b/FE/angular/src/app/components/navigation/go-details/go-details.module.ts new file mode 100644 index 0000000..2e50bf6 --- /dev/null +++ b/FE/angular/src/app/components/navigation/go-details/go-details.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { GoDetailsComponent } from './go-details.component'; + +@NgModule({ + declarations: [GoDetailsComponent], + imports: [CommonModule], + exports: [GoDetailsComponent] +}) +export class GoDetailsModule {} diff --git a/FE/angular/src/app/components/navigation/go-imdb/go-imdb.module.ts b/FE/angular/src/app/components/navigation/go-imdb/go-imdb.module.ts new file mode 100644 index 0000000..83b7b18 --- /dev/null +++ b/FE/angular/src/app/components/navigation/go-imdb/go-imdb.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { GoImdbComponent } from './go-imdb.component'; + +@NgModule({ + declarations: [GoImdbComponent], + imports: [CommonModule], + exports: [GoImdbComponent] +}) +export class GoImdbModule {} diff --git a/FE/angular/src/app/components/navigation/navigation.service.ts b/FE/angular/src/app/components/navigation/navigation.service.ts index 1d6c6a0..c39a73a 100644 --- a/FE/angular/src/app/components/navigation/navigation.service.ts +++ b/FE/angular/src/app/components/navigation/navigation.service.ts @@ -7,8 +7,8 @@ import { Router } from '@angular/router'; export class NavigationService { constructor(private router: Router) {} - public goTo(...args: string[]): Promise { - return this.router.navigate(args).then((event) => { + public goTo(...args: string[]): Promise { + return this.router.navigate(args).then((event: boolean) => { if (!event) { if (isDevMode()) { console.error('Navigation has failed'); diff --git a/FE/angular/src/app/components/sidebar/sidebar.module.ts b/FE/angular/src/app/components/sidebar/sidebar.module.ts new file mode 100644 index 0000000..44ef335 --- /dev/null +++ b/FE/angular/src/app/components/sidebar/sidebar.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { SidebarComponent } from './sidebar.component'; + +@NgModule({ + declarations: [SidebarComponent], + imports: [CommonModule], + exports: [SidebarComponent] +}) +export class SidebarModule {} diff --git a/FE/angular/src/app/constants/constant.ts b/FE/angular/src/app/constants/constant.ts new file mode 100644 index 0000000..629ddf6 --- /dev/null +++ b/FE/angular/src/app/constants/constant.ts @@ -0,0 +1,7 @@ +export class Constants { + static posterUrl = 'https://m.media-amazon.com/images/M/'; + static replacePosterUrl = '/assets/images/'; + static serviceUrl = 'https://www.omdbapi.com/?apikey=f59b2e4b&'; + static moviesHomePage = 'Movies - Home Page'; + static movieDetailPage = 'Movies - Details'; +} diff --git a/FE/angular/src/app/services/data.service.spec.ts b/FE/angular/src/app/services/data.service.spec.ts deleted file mode 100644 index 3f487a2..0000000 --- a/FE/angular/src/app/services/data.service.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { mockProvider, SpectatorService } from '@ngneat/spectator'; -import { createServiceFactory } from '@ngneat/spectator/jest'; -import { of } from 'rxjs'; -import { catchError } from 'rxjs/operators'; -import { DataService } from './data.service'; - -const mockGet = jest.fn().mockReturnValue(of([])); -const mockHttpClient = mockProvider(HttpClient, { - get: mockGet -}); - -const serviceUrl = 'https://www.omdbapi.com/?apikey=f59b2e4b&'; -const mockDecades = [2000, 2010]; -const mockMovies = [ - { - Title: 'Mock Movie', - Year: 2000, - Rated: 'G', - Released: '01 Jan 2000', - Runtime: '90 min', - Genre: 'Mock Genre', - Director: 'Director McMock', - Writer: 'Writer Mock, Writer Mockerson', - Actors: 'Actor McMock, Actor Mockerson', - Plot: 'Mock movie plot summary.', - Poster: - 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', - imdbID: 'tt123', - Type: 'movie' - }, - { - Title: 'Mock Movie 2', - Year: 2011, - Rated: 'G', - Released: '01 Jan 2011', - Runtime: '90 min', - Genre: 'Mock Genre', - Director: 'Director McMock', - Writer: 'Writer Mock, Writer Mockerson', - Actors: 'Actor McMock, Actor Mockerson', - Plot: 'Mock movie plot summary.', - Poster: - 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', - imdbID: 'tt123', - Type: 'movie' - } -]; - -describe('DataService', () => { - let spectator: SpectatorService; - let service: DataService; - const createService = createServiceFactory({ - service: DataService, - imports: [], - declarations: [], - providers: [mockHttpClient] - }); - - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - spectator = createService(); - service = spectator.service; - }); - - test('should create the service', () => { - expect(service).toBeTruthy(); - }); - - describe('getFilteredMovies', () => { - describe('WHEN decade is undefined', () => { - test('should return all movies', () => { - expect(service.getFilteredMovies(mockMovies)).toEqual(mockMovies); - }); - }); - describe('WHEN decade is defined', () => { - test('should return only movies from that decade', () => { - expect(service.getFilteredMovies(mockMovies, 2010)).toEqual([mockMovies[1]]); - }); - }); - }); - - describe('getMovie', () => { - const mockMovie = mockMovies[0]; - beforeEach(() => { - mockGet.mockReturnValueOnce(of(mockMovie)); - service.getMovie(mockMovie.imdbID); - }); - test('should call http.get', () => { - expect(mockGet).toBeCalledWith(`${serviceUrl}i=${mockMovie.imdbID}`); - }); - }); - - describe('getMovies', () => { - beforeEach(() => { - mockGet.mockReturnValueOnce(of({ Response: 'True', Search: mockMovies, totalResults: '2' })); - mockGet.mockReturnValue(of(mockMovies[1])); - service.getMovies(); - }); - test('should call http.get', () => { - expect(mockGet).toBeCalledWith(`${serviceUrl}s=Batman&type=movie`); - }); - }); -}); diff --git a/FE/angular/src/app/services/data.service.ts b/FE/angular/src/app/services/data.service.ts deleted file mode 100644 index 93dedc4..0000000 --- a/FE/angular/src/app/services/data.service.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable, isDevMode } from '@angular/core'; -import { forkJoin, Observable, of, throwError } from 'rxjs'; -import { catchError, map, mergeMap } from 'rxjs/operators'; - -interface SearchResults { - Response: string; - Search: Movie[]; - totalResults: string; -} - -interface Movie { - imdbID: string; - Poster: string; - Title: string; - Type: string; - Year: string | number; -} - -interface MovieDetails extends Movie { - Actors: string; - Director: string; - Genre: string; - Plot: string; - Rated: string; - Released: string; - Runtime: string; - Writer: string; -} - -export interface MovieComplete extends MovieDetails { - Year: number; -} - -export interface MovieData { - Decades: number[]; - Search: MovieComplete[]; -} - -@Injectable({ - providedIn: 'root' -}) -export class DataService { - private decades: number[] = []; - private posterUrl = 'https://m.media-amazon.com/images/M/'; - private replacePosterUrl = '/assets/images/'; - private serviceUrl = 'https://www.omdbapi.com/?apikey=f59b2e4b&'; - private storedMovies: MovieData = { Search: [], Decades: [] }; - - constructor(private http: HttpClient) {} - - public getFilteredMovies(movies: MovieComplete[], decade?: number): MovieComplete[] { - if (!decade) { - return movies; - } - - const decadeLimit = decade + 10; - return movies.filter((movie) => movie.Year >= decade && movie.Year < decadeLimit); - } - - public getMovie(id: string): Observable { - return this.http.get(`${this.serviceUrl}i=${id}`).pipe( - map(({ Actors, Director, Genre, imdbID, Plot, Poster, Rated, Released, Runtime, Title, Type, Writer, Year }) => ({ - Actors, - Director, - Genre, - imdbID, - Plot, - Poster: Poster.replace(this.posterUrl, this.replacePosterUrl), - Rated, - Released, - Runtime, - Title, - Type, - Writer, - Year: parseInt(Year as string) - })) - ); - } - - public getMovies(): Observable { - if (this.storedMovies && this.storedMovies.Search.length) { - return of(this.storedMovies); - } - - return this.http.get(`${this.serviceUrl}s=Batman&type=movie`).pipe( - mergeMap(({ Search }) => - forkJoin( - Search.map(({ imdbID, Year }) => { - // add decade to decades - const decade = Math.ceil(parseInt(Year as string) / 10) * 10 - 10; - if (this.decades.indexOf(decade) < 0) { - this.decades.push(decade); - } - - return this.getMovie(imdbID); - }) - ) - ), - map((Search) => { - Search = Search.sort(({ Year: year1 }: MovieComplete, { Year: year2 }: MovieComplete) => year1 - year2); - this.decades.sort((a, b) => a - b); - this.storedMovies = { Search, Decades: this.decades }; - - return this.storedMovies; - }) - ); - } -} diff --git a/FE/angular/src/app/tests/mock-data.ts b/FE/angular/src/app/tests/mock-data.ts new file mode 100644 index 0000000..aaabb2d --- /dev/null +++ b/FE/angular/src/app/tests/mock-data.ts @@ -0,0 +1,35 @@ +export const mockDecades = [2000]; +export const mockMovies = [ + { + Title: 'Mock Movie', + Year: 2000, + Rated: 'G', + Released: '01 Jan 2000', + Runtime: '90 min', + Genre: 'Mock Genre', + Director: 'Director McMock', + Writer: 'Writer Mock, Writer Mockerson', + Actors: 'Actor McMock, Actor Mockerson', + Plot: 'Mock movie plot summary.', + Poster: + 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', + imdbID: 'tt123', + Type: 'movie' + }, + { + Title: 'Mock Movie 2', + Year: 2011, + Rated: 'G', + Released: '01 Jan 2011', + Runtime: '90 min', + Genre: 'Mock Genre', + Director: 'Director McMock', + Writer: 'Writer Mock, Writer Mockerson', + Actors: 'Actor McMock, Actor Mockerson', + Plot: 'Mock movie plot summary.', + Poster: + 'https://m.media-amazon.com/images/M/MV5BOTY4YjI2N2MtYmFlMC00ZjcyLTg3YjEtMDQyM2ZjYzQ5YWFkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg', + imdbID: 'tt123', + Type: 'movie' + } +]; diff --git a/FE/angular/src/assets/images/MV5BM2MyNTAwZGEtNTAxNC00ODVjLTgzZjUtYmU0YjAzNmQyZDEwXkEyXkFqcGdeQXVyNDc2NTg3NzA@._V1_SX300.jpg b/FE/angular/src/assets/images/MV5BM2MyNTAwZGEtNTAxNC00ODVjLTgzZjUtYmU0YjAzNmQyZDEwXkEyXkFqcGdeQXVyNDc2NTg3NzA@._V1_SX300.jpg new file mode 100644 index 0000000..3e474b3 Binary files /dev/null and b/FE/angular/src/assets/images/MV5BM2MyNTAwZGEtNTAxNC00ODVjLTgzZjUtYmU0YjAzNmQyZDEwXkEyXkFqcGdeQXVyNDc2NTg3NzA@._V1_SX300.jpg differ diff --git a/FE/angular/src/assets/images/MV5BOTRlNWQwM2ItNjkyZC00MGI3LThkYjktZmE5N2FlMzcyNTIyXkEyXkFqcGdeQXVyMTEyNzgwMDUw._V1_SX300.jpg b/FE/angular/src/assets/images/MV5BOTRlNWQwM2ItNjkyZC00MGI3LThkYjktZmE5N2FlMzcyNTIyXkEyXkFqcGdeQXVyMTEyNzgwMDUw._V1_SX300.jpg new file mode 100644 index 0000000..ca39a0e Binary files /dev/null and b/FE/angular/src/assets/images/MV5BOTRlNWQwM2ItNjkyZC00MGI3LThkYjktZmE5N2FlMzcyNTIyXkEyXkFqcGdeQXVyMTEyNzgwMDUw._V1_SX300.jpg differ diff --git a/FE/angular/src/assets/images/MV5BZWQ0OTQ3ODctMmE0MS00ODc2LTg0ZTEtZWIwNTUxOGExZTQ4XkEyXkFqcGdeQXVyNzAwMjU2MTY@._V1_SX300.jpg b/FE/angular/src/assets/images/MV5BZWQ0OTQ3ODctMmE0MS00ODc2LTg0ZTEtZWIwNTUxOGExZTQ4XkEyXkFqcGdeQXVyNzAwMjU2MTY@._V1_SX300.jpg new file mode 100644 index 0000000..cc2c4db Binary files /dev/null and b/FE/angular/src/assets/images/MV5BZWQ0OTQ3ODctMmE0MS00ODc2LTg0ZTEtZWIwNTUxOGExZTQ4XkEyXkFqcGdeQXVyNzAwMjU2MTY@._V1_SX300.jpg differ diff --git a/FE/angular/src/assets/images/placeholder.jpg b/FE/angular/src/assets/images/placeholder.jpg new file mode 100644 index 0000000..3f16474 Binary files /dev/null and b/FE/angular/src/assets/images/placeholder.jpg differ diff --git a/FE/angular/src/styles/components/_movie.scss b/FE/angular/src/styles/components/_movie.scss index 042944c..d088208 100644 --- a/FE/angular/src/styles/components/_movie.scss +++ b/FE/angular/src/styles/components/_movie.scss @@ -4,7 +4,7 @@ max-width: 800px; margin-bottom: 28px; padding: $mainFontSize; - flex-direction: column; + flex-direction: row; background: $paleGray; border-radius: $radius; border: 2px solid $darkerGray; @@ -27,6 +27,9 @@ font-weight: $fontWeightBold; height: $wideSpace; border-bottom: 1px solid $darkGray; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } .details, .metadata { diff --git a/FE/angular/src/styles/layout/_elements.scss b/FE/angular/src/styles/layout/_elements.scss index 3f23515..d838087 100644 --- a/FE/angular/src/styles/layout/_elements.scss +++ b/FE/angular/src/styles/layout/_elements.scss @@ -16,7 +16,7 @@ button { &.imdb, &.detail { background: #f9b44a; - color: #fff; + color: #4A4A4A; font-family: $fontStackBold; font-size: 16px; padding: 15px 45px;