Add IMDb movie importer feature and unify header

Introduces a new movie importer feature allowing admins to search and import movies from IMDb, including new components for search, result display, and error handling. Replaces the schedule header with a reusable menu header component. Updates routing, navigation, and HTTP service to support the new importer. Adds a poster placeholder image and improves poster error handling.
This commit is contained in:
2025-11-07 01:57:42 +01:00
parent bd7a0ed9f1
commit 4f5a8e9661
25 changed files with 338 additions and 42 deletions

36
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.84", "@infinimotion/model-frontend": "^0.0.85",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
@@ -400,6 +400,7 @@
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.9.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.9.tgz",
"integrity": "sha512-rbY1AMz9389WJI29iAjWp4o0QKRQHCrQQUuP0ctNQzh1tgWpwiRLx8N4yabdVdsCA846vPsyKJtBlSNwKMsjJA==", "integrity": "sha512-rbY1AMz9389WJI29iAjWp4o0QKRQHCrQQUuP0ctNQzh1tgWpwiRLx8N4yabdVdsCA846vPsyKJtBlSNwKMsjJA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"parse5": "^8.0.0", "parse5": "^8.0.0",
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -446,6 +447,7 @@
"node_modules/@angular/common": { "node_modules/@angular/common": {
"version": "20.3.5", "version": "20.3.5",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -460,6 +462,7 @@
"node_modules/@angular/compiler": { "node_modules/@angular/compiler": {
"version": "20.3.5", "version": "20.3.5",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -471,6 +474,7 @@
"version": "20.3.5", "version": "20.3.5",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/core": "7.28.3", "@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14", "@jridgewell/sourcemap-codec": "^1.4.14",
@@ -501,6 +505,7 @@
"node_modules/@angular/core": { "node_modules/@angular/core": {
"version": "20.3.5", "version": "20.3.5",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -524,6 +529,7 @@
"node_modules/@angular/forms": { "node_modules/@angular/forms": {
"version": "20.3.5", "version": "20.3.5",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -557,6 +563,7 @@
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "20.3.5", "version": "20.3.5",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@@ -615,6 +622,7 @@
"version": "7.28.3", "version": "7.28.3",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
@@ -1290,9 +1298,9 @@
} }
}, },
"node_modules/@infinimotion/model-frontend": { "node_modules/@infinimotion/model-frontend": {
"version": "0.0.84", "version": "0.0.85",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.84/model-frontend-0.0.84.tgz", "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.85/model-frontend-0.0.85.tgz",
"integrity": "sha512-+SMFsobPpfh6H9cU54DfVl9sF9Mp1vj6HuB135Y+grWvk/nIN4wzzZLvYPIk3BDURTT1DHgg8O3m66FaBB22sQ==", "integrity": "sha512-QPiZNl//Y1JdxtXk+VScc67h1K664z68PUCXRff9fRf4IHlYXtqutc+ainK8vxOVSqqL6EEmDAtbLsRwrG6kRg==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@inquirer/ansi": { "node_modules/@inquirer/ansi": {
@@ -1507,6 +1515,7 @@
"version": "7.8.2", "version": "7.8.2",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@inquirer/checkbox": "^4.2.1", "@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14", "@inquirer/confirm": "^5.1.14",
@@ -3593,6 +3602,7 @@
"version": "24.8.0", "version": "24.8.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.14.0" "undici-types": "~7.14.0"
} }
@@ -3878,6 +3888,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.9", "baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746", "caniuse-lite": "^1.0.30001746",
@@ -4766,6 +4777,7 @@
"version": "5.1.0", "version": "5.1.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@@ -5571,7 +5583,8 @@
"node_modules/jasmine-core": { "node_modules/jasmine-core": {
"version": "5.9.0", "version": "5.9.0",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.6.1", "version": "2.6.1",
@@ -5645,6 +5658,7 @@
"version": "6.4.4", "version": "6.4.4",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "1.5.0", "@colors/colors": "1.5.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
@@ -6270,6 +6284,7 @@
"version": "9.0.1", "version": "9.0.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cli-truncate": "^4.0.0", "cli-truncate": "^4.0.0",
"colorette": "^2.0.20", "colorette": "^2.0.20",
@@ -7728,6 +7743,7 @@
"node_modules/rxjs": { "node_modules/rxjs": {
"version": "7.8.2", "version": "7.8.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
@@ -7776,6 +7792,7 @@
"version": "1.90.0", "version": "1.90.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^5.0.2", "immutable": "^5.0.2",
@@ -8432,7 +8449,8 @@
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.8.1",
"license": "0BSD" "license": "0BSD",
"peer": true
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "3.1.0", "version": "3.1.0",
@@ -8464,6 +8482,7 @@
"version": "5.9.3", "version": "5.9.3",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -8622,6 +8641,7 @@
"version": "7.1.5", "version": "7.1.5",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -8971,6 +8991,7 @@
"version": "3.25.76", "version": "3.25.76",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
@@ -8985,7 +9006,8 @@
}, },
"node_modules/zone.js": { "node_modules/zone.js": {
"version": "0.15.1", "version": "0.15.1",
"license": "MIT" "license": "MIT",
"peer": true
} }
} }
} }

View File

@@ -30,7 +30,7 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.84", "@infinimotion/model-frontend": "^0.0.85",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",

View File

@@ -16,7 +16,7 @@ import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field'; 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 { MatDividerModule } from '@angular/material/divider';
import { HeaderComponent } from './header/header.component'; 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 { SeatRowComponent } from './seat-row/seat-row.component';
import { TheaterLayoutComponent } from './theater-layout/theater-layout.component'; import { TheaterLayoutComponent } from './theater-layout/theater-layout.component';
import { MovieSearchComponent } from './movie-search/movie-search.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 { 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({ @NgModule({
@@ -69,8 +72,11 @@ import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search
SeatRowComponent, SeatRowComponent,
TheaterLayoutComponent, TheaterLayoutComponent,
MovieSearchComponent, MovieSearchComponent,
ScheduleHeaderComponent, MenuHeaderComponent,
MovieScheduleNoSearchResultComponent MovieScheduleNoSearchResultComponent,
MovieImporterComponent,
MovieImportNoSearchResultComponent,
MovieImportSearchInfoComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
@@ -87,7 +93,8 @@ import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search
MatInputModule, MatInputModule,
MatFormFieldModule, MatFormFieldModule,
MatIconButton, MatIconButton,
MatDividerModule MatDividerModule,
MatButtonModule
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),

View File

@@ -6,6 +6,7 @@ import { MainLayoutComponent } from './layouts/main-layout/main-layout.component
import { MainComponent } from './main/main.component'; import { MainComponent } from './main/main.component';
import { ScheduleComponent } from './schedule/schedule.component'; import { ScheduleComponent } from './schedule/schedule.component';
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component'; import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
const routes: Routes = [ const routes: Routes = [
// Seiten ohne Layout // Seiten ohne Layout
@@ -19,6 +20,7 @@ const routes: Routes = [
children: [ children: [
{ path: '', component: MainComponent }, { path: '', component: MainComponent },
{ path: 'schedule', component: ScheduleComponent }, { path: 'schedule', component: ScheduleComponent },
{ path: 'admin/movie-importer', component: MovieImporterComponent },
{ path: 'theater-overlay', component: TheaterOverlayComponent}, { path: 'theater-overlay', component: TheaterOverlayComponent},
], ],
}, },

View File

@@ -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 { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core"; import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
@@ -52,7 +52,7 @@ export class HttpService {
} }
/* POST /api/vorstellung/filter */ /* POST /api/vorstellung/filter */
getPerformaceByFilter(filter: string[]): Observable<Vorstellung[]> { getPerformacesByFilter(filter: string[]): Observable<Vorstellung[]> {
return this.http.post<Vorstellung[]>(`${this.baseUrl}vorstellung/filter`, filter); 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`); 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} */ /* GET /api/show-seats/{show} */
getSeatsByShowId(show: number): Observable<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}> { 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}`); 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}`, {})
}
} }

View 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>

View 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>();
}

View File

@@ -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>

View File

@@ -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>();
}

View File

@@ -0,0 +1,4 @@
:host {
display: block;
margin: 60px 0;
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,3 @@
:host {
min-height: 100%;
}

View 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>

View 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;
}
}

View File

@@ -1,5 +1,10 @@
<div class="w-64 mx-auto my-2"> <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>
<div class="flex gap-1 justify-between"> <div class="flex gap-1 justify-between">
<app-movie-rating [rating]="movie().rating"></app-movie-rating> <app-movie-rating [rating]="movie().rating"></app-movie-rating>

View File

@@ -9,4 +9,13 @@ import { Film } from '@infinimotion/model-frontend';
}) })
export class MoviePosterComponent { export class MoviePosterComponent {
movie = input.required<Film>(); 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;
}
}
} }

View File

@@ -13,6 +13,7 @@ export class NavbarComponent {
navItems:{label:string, path:string}[] = [ navItems:{label:string, path:string}[] = [
{label: 'Programm', path: '/schedule'}, {label: 'Programm', path: '/schedule'},
{label: 'Film importieren', path: '/admin/movie-importer'},
{label: 'Kinosaal-test', path: '/theater-overlay'}, {label: 'Kinosaal-test', path: '/theater-overlay'},
] ]
} }

View File

@@ -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>

View File

@@ -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>();
}

View File

@@ -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> <mat-tab-group mat-stretch-tabs>
@for (dateInfo of dates; track dateInfo.date; let i = $index) { @for (dateInfo of dates; track dateInfo.date; let i = $index) {

View File

@@ -4,7 +4,7 @@ import { Vorstellung } from '@infinimotion/model-frontend';
import { Performance } from '../model/performance.model'; import { Performance } from '../model/performance.model';
import { MovieGroup } from '../model/movie-group.model'; import { MovieGroup } from '../model/movie-group.model';
import { LoadingService } from '../loading.service'; import { LoadingService } from '../loading.service';
import { catchError, map, of, tap } from 'rxjs'; import { catchError, of, tap } from 'rxjs';
@Component({ @Component({
selector: 'app-schedule', selector: 'app-schedule',
@@ -61,8 +61,7 @@ export class ScheduleComponent implements OnInit {
loadPerformances(bookableDays: number) { loadPerformances(bookableDays: number) {
this.loading.show(); this.loading.show();
const filter = this.generateDateFilter(bookableDays); const filter = this.generateDateFilter(bookableDays);
this.http.getPerformaceByFilter(filter).pipe( this.http.getPerformacesByFilter(filter).pipe(
map(data => Array.isArray(data) ? data : [data]),
tap(performaces => { tap(performaces => {
this.performaces = performaces; this.performaces = performaces;
this.assignPerformancesToDates(); this.assignPerformancesToDates();

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB