Merge branch 'movie-importer'
This commit is contained in:
@@ -16,7 +16,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatButtonModule, MatIconButton } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
@@ -41,8 +41,11 @@ import { SeatComponent } from './seat/seat.component';
|
||||
import { SeatRowComponent } from './seat-row/seat-row.component';
|
||||
import { TheaterLayoutComponent } from './theater-layout/theater-layout.component';
|
||||
import { MovieSearchComponent } from './movie-search/movie-search.component';
|
||||
import { ScheduleHeaderComponent } from './schedule-header/schedule-header.component';
|
||||
import { MenuHeaderComponent } from './menu-header/menu-header.component';
|
||||
import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search-result/movie-schedule-no-search-result.component';
|
||||
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||
import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component';
|
||||
import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -69,8 +72,11 @@ import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search
|
||||
SeatRowComponent,
|
||||
TheaterLayoutComponent,
|
||||
MovieSearchComponent,
|
||||
ScheduleHeaderComponent,
|
||||
MovieScheduleNoSearchResultComponent
|
||||
MenuHeaderComponent,
|
||||
MovieScheduleNoSearchResultComponent,
|
||||
MovieImporterComponent,
|
||||
MovieImportNoSearchResultComponent,
|
||||
MovieImportSearchInfoComponent,
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
@@ -87,7 +93,8 @@ import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatIconButton,
|
||||
MatDividerModule
|
||||
MatDividerModule,
|
||||
MatButtonModule
|
||||
],
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MainLayoutComponent } from './layouts/main-layout/main-layout.component
|
||||
import { MainComponent } from './main/main.component';
|
||||
import { ScheduleComponent } from './schedule/schedule.component';
|
||||
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
|
||||
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||
|
||||
const routes: Routes = [
|
||||
// Seiten ohne Layout
|
||||
@@ -19,6 +20,7 @@ const routes: Routes = [
|
||||
children: [
|
||||
{ path: '', component: MainComponent },
|
||||
{ path: 'schedule', component: ScheduleComponent },
|
||||
{ path: 'admin/movie-importer', component: MovieImporterComponent },
|
||||
{ path: 'selection/performance/:id', component: TheaterOverlayComponent}, //?
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Kinosaal, Sitzplatz, Vorstellung, Film } from '@infinimotion/model-frontend';
|
||||
import { Kinosaal, Sitzplatz, Vorstellung, Film, OmdbSearch } from '@infinimotion/model-frontend';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
@@ -52,7 +52,7 @@ export class HttpService {
|
||||
}
|
||||
|
||||
/* POST /api/vorstellung/filter */
|
||||
getPerformaceByFilter(filter: string[]): Observable<Vorstellung[]> {
|
||||
getPerformacesByFilter(filter: string[]): Observable<Vorstellung[]> {
|
||||
return this.http.post<Vorstellung[]>(`${this.baseUrl}vorstellung/filter`, filter);
|
||||
}
|
||||
|
||||
@@ -79,11 +79,31 @@ export class HttpService {
|
||||
return this.http.get<Film[]>(`${this.baseUrl}film`);
|
||||
}
|
||||
|
||||
/* POST /api/vorstellung/filter */
|
||||
getMoviesByFilter(filter: string[]): Observable<Film[]> {
|
||||
return this.http.post<Film[]>(`${this.baseUrl}film/filter`, filter);
|
||||
}
|
||||
|
||||
/* Show-Seats APIs*/
|
||||
|
||||
/* Show-Seats APIs */
|
||||
|
||||
/* GET /api/show-seats/{show} */
|
||||
getSeatsByShowId(show: number): Observable<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}> {
|
||||
return this.http.get<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}>(`${this.baseUrl}show-seats/${show}`);
|
||||
}
|
||||
|
||||
|
||||
/* Movie Importer APIs */
|
||||
|
||||
/* GET /api/importer/search */
|
||||
searchMovie(query: string): Observable<OmdbSearch> {
|
||||
return this.http.get<OmdbSearch>(`${this.baseUrl}importer/search`, {
|
||||
params: { title: query }
|
||||
});
|
||||
}
|
||||
|
||||
/* POST /api/importer/import */
|
||||
importMovie(imdbId: string): Observable<Film> {
|
||||
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {})
|
||||
}
|
||||
}
|
||||
|
||||
14
src/app/menu-header/menu-header.component.html
Normal file
14
src/app/menu-header/menu-header.component.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="schedule-header px-4 py-4">
|
||||
<div class="flex items-center">
|
||||
@if ( icon() ) {
|
||||
<mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">{{ icon() }}</mat-icon>
|
||||
}
|
||||
<p class="text-2xl font-medium pl-2 bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
{{ title() }}
|
||||
</p>
|
||||
</div>
|
||||
@if ( searchBar() ) {
|
||||
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
|
||||
}
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
15
src/app/menu-header/menu-header.component.ts
Normal file
15
src/app/menu-header/menu-header.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu-header',
|
||||
standalone: false,
|
||||
templateUrl: './menu-header.component.html',
|
||||
styleUrl: './menu-header.component.css'
|
||||
})
|
||||
export class MenuHeaderComponent {
|
||||
title = input.required<string>();
|
||||
icon = input<string>();
|
||||
|
||||
searchBar = input<boolean>(false);
|
||||
movieSearchResult = output<string>();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="flex flex-col items-center justify-center py-12 text-gray-500 h-100 m-auto">
|
||||
<mat-icon class="text-6xl mb-4 opacity-50" style="font-size: 50px; width: 50px; height: 50px;">search_off</mat-icon>
|
||||
<p class="text-lg">Kein Film gefunden</p>
|
||||
<p class="text-sm">Für '{{ search() }}' konnten über IMDb keine Filme gefunden werden.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-import-no-search-result',
|
||||
standalone: false,
|
||||
templateUrl: './movie-import-no-search-result.component.html',
|
||||
styleUrl: './movie-import-no-search-result.component.css'
|
||||
})
|
||||
export class MovieImportNoSearchResultComponent {
|
||||
search = input.required<string>();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: block;
|
||||
margin: 60px 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="flex flex-col md:flex-row gap-1">
|
||||
<div class="w-1/5">
|
||||
<img
|
||||
[src]="movie().poster && movie().poster !== 'N/A' ? movie().poster :'assets/poster_placeholder.png'"
|
||||
alt="Movie Poster"
|
||||
class="w-40 h-auto shadow-md rounded-md"
|
||||
(error)="onPosterError($event)"
|
||||
>
|
||||
</div>
|
||||
<div class="mx-8 my-auto w-4/5">
|
||||
<h1 class="text-4xl font-bold mb-2">{{ movie().title }}</h1>
|
||||
<h2 class="text-xl mb-8">
|
||||
Erscheinungsjahr: {{ movie().year }}
|
||||
</h2>
|
||||
|
||||
<button matFab extended class="mb-3" (click)="importMovie(movie().imdbID, movie().title)" [disabled]="this.buttonDisabled">
|
||||
<mat-icon>{{ buttonIcon }}</mat-icon>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { Film, OmdbMovie } from '@infinimotion/model-frontend';
|
||||
import { HttpService } from '../http.service';
|
||||
import { LoadingService } from '../loading.service';
|
||||
import { catchError, EMPTY, of, switchMap, tap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-import-search-info',
|
||||
standalone: false,
|
||||
templateUrl: './movie-import-search-info.component.html',
|
||||
styleUrl: './movie-import-search-info.component.css'
|
||||
})
|
||||
export class MovieImportSearchInfoComponent {
|
||||
readonly movie = input.required<OmdbMovie>();
|
||||
|
||||
importedMovie: Film | undefined;
|
||||
buttonDisabled = false;
|
||||
buttonText: string = "Film von IMDb importieren";
|
||||
buttonIcon: string = "cloud_download"
|
||||
|
||||
private http = inject(HttpService)
|
||||
private loading = inject(LoadingService)
|
||||
|
||||
importMovie(imdbId: string, title: string) {
|
||||
this.buttonDisabled = true;
|
||||
this.loading.show();
|
||||
|
||||
const filter = this.generateTitleFilter(title);
|
||||
|
||||
this.http.getMoviesByFilter(filter).pipe(
|
||||
switchMap(movies => {
|
||||
if (movies.length !== 0) {
|
||||
this.loading.showError(`Dublette erkannt! Film '${title}' existiert bereits.`);
|
||||
console.warn('Dublette erkannt. Import abgebrochen.');
|
||||
this.buttonText = 'Film existiert bereits'
|
||||
this.buttonIcon = 'file_present';
|
||||
return EMPTY;
|
||||
}
|
||||
return this.http.importMovie(imdbId);
|
||||
}),
|
||||
tap(importedMovie => {
|
||||
this.importedMovie = importedMovie;
|
||||
this.buttonText = `Film erfolgreich importiert (Id: ${importedMovie.id})`;
|
||||
this.buttonIcon = 'cloud_done';
|
||||
this.loading.hide();
|
||||
}),
|
||||
catchError(err => {
|
||||
this.buttonDisabled = false;
|
||||
this.loading.showError(err);
|
||||
console.error('Fehler beim Import oder bei der Dublettenprüfung', err);
|
||||
return of([]);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private generateTitleFilter(title: string): string[] {
|
||||
return [`eq;title;string;${title}`]
|
||||
}
|
||||
|
||||
onPosterError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
const placeholder = 'assets/poster_placeholder.png';
|
||||
|
||||
if (img.src !== window.location.origin + placeholder) {
|
||||
img.src = placeholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/app/movie-importer/movie-importer.component.css
Normal file
3
src/app/movie-importer/movie-importer.component.css
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
min-height: 100%;
|
||||
}
|
||||
52
src/app/movie-importer/movie-importer.component.html
Normal file
52
src/app/movie-importer/movie-importer.component.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<app-menu-header title="Film aus IMDb importieren" icon="cloud_download"></app-menu-header>
|
||||
|
||||
<div class="w-6/10 m-auto my-20">
|
||||
<form class="movie-search-form w-full" (ngSubmit)="DoSubmit()">
|
||||
<div class="flex items-center space-x-4">
|
||||
<mat-form-field class="w-full" subscriptSizing="dynamic">
|
||||
<mat-label>Film online suchen</mat-label>
|
||||
<input class="w-full" type="text" matInput [formControl]="formControl">
|
||||
@if (formControl.hasError('noResults')) {
|
||||
<mat-error>Keine Suchergebnisse gefunden</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button matFab [disabled]="!formControl.value?.trim()?.length || (loadingService.loading$ | async)" type="submit">
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (search_query.length > 0 && !isSearching) {
|
||||
<div class="search-results my-20">
|
||||
|
||||
@if (movies.length > 0) {
|
||||
|
||||
<!-- 1. Ergebnis -->
|
||||
<app-movie-import-search-info [movie]="movies[0]" class="py-3"></app-movie-import-search-info>
|
||||
|
||||
@if (movies.length > 1 && !showAll) {
|
||||
<div class="text-blue-500 cursor-pointer mt-2 w-fit" (click)="toggleShowAll()">
|
||||
{{ movies.length - 1 }} weitere Suchergebnisse anzeigen
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Weitere Ergebnisse -->
|
||||
@if (showAll) {
|
||||
@for (movie of movies.slice(1); track movie.imdbID) {
|
||||
<app-movie-import-search-info [movie]="movie" class="py-3"></app-movie-import-search-info>
|
||||
}
|
||||
<div class="text-blue-500 cursor-pointer mt-20 mb-30 w-fit" (click)="toggleShowAll()">
|
||||
Weitere Suchergebnisse ausblenden
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Keine Ergebnisse -->
|
||||
@else {
|
||||
<app-movie-import-no-search-result [search]="search_query"></app-movie-import-no-search-result>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
58
src/app/movie-importer/movie-importer.component.ts
Normal file
58
src/app/movie-importer/movie-importer.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { LoadingService } from './../loading.service';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { catchError, finalize, of, tap } from 'rxjs';
|
||||
import { HttpService } from '../http.service';
|
||||
import { OmdbMovie } from '@infinimotion/model-frontend';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-importer',
|
||||
standalone: false,
|
||||
templateUrl: './movie-importer.component.html',
|
||||
styleUrl: './movie-importer.component.css'
|
||||
})
|
||||
export class MovieImporterComponent {
|
||||
formControl = new FormControl('')
|
||||
|
||||
movies: OmdbMovie[] = [];
|
||||
search_query: string = '';
|
||||
showAll = false;
|
||||
isSearching = false;
|
||||
|
||||
private httpService = inject(HttpService)
|
||||
public loadingService = inject(LoadingService)
|
||||
|
||||
DoSubmit() {
|
||||
this.showAll = false;
|
||||
this.searchForMovies();
|
||||
}
|
||||
|
||||
searchForMovies() {
|
||||
this.search_query = this.formControl.value?.trim() || '';
|
||||
if (this.search_query?.length == 0) return;
|
||||
|
||||
this.isSearching = true;
|
||||
this.formControl.disable();
|
||||
this.loadingService.show();
|
||||
|
||||
this.httpService.searchMovie(this.search_query).pipe(
|
||||
tap(movies => {
|
||||
this.movies = movies.search || [] ;
|
||||
this.loadingService.hide();
|
||||
}),
|
||||
catchError(err => {
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler bei der Suchen', err);
|
||||
return of([]);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isSearching = false;
|
||||
this.formControl.enable();
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
toggleShowAll() {
|
||||
this.showAll = !this.showAll;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
<div class="w-64 mx-auto my-2">
|
||||
<img [src]="movie().image" alt="Movie Poster" class="w-full h-auto shadow-md">
|
||||
<img
|
||||
[src]="movie().image && movie().image !== 'N/A' ? movie().image : 'assets/poster_placeholder.png'"
|
||||
alt="Movie Poster"
|
||||
class="w-full h-auto shadow-md rounded-md"
|
||||
(error)="onPosterError($event)"
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-between">
|
||||
<app-movie-rating [rating]="movie().rating"></app-movie-rating>
|
||||
|
||||
@@ -9,4 +9,13 @@ import { Film } from '@infinimotion/model-frontend';
|
||||
})
|
||||
export class MoviePosterComponent {
|
||||
movie = input.required<Film>();
|
||||
|
||||
onPosterError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
const placeholder = 'assets/poster_placeholder.png';
|
||||
|
||||
if (img.src !== window.location.origin + placeholder) {
|
||||
img.src = placeholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@ import { Component } from '@angular/core';
|
||||
styleUrl: './navbar.component.css'
|
||||
})
|
||||
export class NavbarComponent {
|
||||
/*
|
||||
die routen abfragen und die routen namen verwenden?
|
||||
*/
|
||||
|
||||
navItems:{label:string, path:string}[] = [
|
||||
navItems: { label:string, path:string }[] = [
|
||||
{label: 'Programm', path: '/schedule'},
|
||||
{label: 'Film importieren', path: '/admin/movie-importer'},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="schedule-header px-4 py-4 duration-1000">
|
||||
<div class="flex items-center">
|
||||
<mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">event</mat-icon>
|
||||
<p class="text-2xl font-medium pl-2 bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
Programmübersicht
|
||||
</p>
|
||||
</div>
|
||||
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-header',
|
||||
standalone: false,
|
||||
templateUrl: './schedule-header.component.html',
|
||||
styleUrl: './schedule-header.component.css'
|
||||
})
|
||||
export class ScheduleHeaderComponent {
|
||||
movieSearchResult = output<string>();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<app-schedule-header (movieSearchResult)="movieSearchResult = $event"></app-schedule-header>
|
||||
<app-menu-header title="Programmübersicht" icon="event" [searchBar]="true" (movieSearchResult)="movieSearchResult = $event"></app-menu-header>
|
||||
|
||||
<mat-tab-group mat-stretch-tabs>
|
||||
@for (dateInfo of dates; track dateInfo.date; let i = $index) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { Performance } from '../model/performance.model';
|
||||
import { MovieGroup } from '../model/movie-group.model';
|
||||
import { LoadingService } from '../loading.service';
|
||||
import { catchError, map, of, tap } from 'rxjs';
|
||||
import { catchError, of, tap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule',
|
||||
@@ -61,8 +61,7 @@ export class ScheduleComponent implements OnInit {
|
||||
loadPerformances(bookableDays: number) {
|
||||
this.loading.show();
|
||||
const filter = this.generateDateFilter(bookableDays);
|
||||
this.http.getPerformaceByFilter(filter).pipe(
|
||||
map(data => Array.isArray(data) ? data : [data]),
|
||||
this.http.getPerformacesByFilter(filter).pipe(
|
||||
tap(performaces => {
|
||||
this.performaces = performaces;
|
||||
this.assignPerformancesToDates();
|
||||
|
||||
Reference in New Issue
Block a user