Add movie search and schedule header components

Introduces MovieSearchComponent, ScheduleHeaderComponent, and MovieScheduleNoSearchResultComponent for improved movie search and schedule display. Updates schedule and navbar to support search functionality and UI enhancements. Adds movie fetching to HttpService and refines layout and styles for better user experience.
This commit is contained in:
2025-11-05 01:04:56 +01:00
parent 5cf71a43ed
commit 0bd3887701
17 changed files with 174 additions and 12 deletions

View File

@@ -1,9 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core'; import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { provideHttpClient, withFetch } from '@angular/common/http'; import { provideHttpClient, withFetch } from '@angular/common/http';
import { AppRoutingModule } from './app-routing-module'; import { AppRoutingModule } from './app-routing-module';
import { App } from './app'; import { App } from './app';
@@ -12,7 +13,11 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSnackBarModule } from '@angular/material/snack-bar'; 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 { MatDividerModule } from '@angular/material/divider';
import { HeaderComponent } from './header/header.component'; import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
@@ -34,8 +39,10 @@ import { Header2Component } from './header-2/header-2.component';
import { TheaterOverlayComponent } from './theater-overlay/theater-overlay.component'; import { TheaterOverlayComponent } from './theater-overlay/theater-overlay.component';
import { SeatComponent } from './seat/seat.component'; import { SeatComponent } from './seat/seat.component';
import { SeatRowComponent } from './seat-row/seat-row.component'; import { SeatRowComponent } from './seat-row/seat-row.component';
import {MatIconButton} from '@angular/material/button';
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 { ScheduleHeaderComponent } from './schedule-header/schedule-header.component';
import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search-result/movie-schedule-no-search-result.component';
@NgModule({ @NgModule({
@@ -60,11 +67,15 @@ import { TheaterLayoutComponent } from './theater-layout/theater-layout.componen
TheaterOverlayComponent, TheaterOverlayComponent,
SeatComponent, SeatComponent,
SeatRowComponent, SeatRowComponent,
TheaterLayoutComponent TheaterLayoutComponent,
MovieSearchComponent,
ScheduleHeaderComponent,
MovieScheduleNoSearchResultComponent
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
BrowserModule, BrowserModule,
ReactiveFormsModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
MatIconModule, MatIconModule,
@@ -72,7 +83,11 @@ import { TheaterLayoutComponent } from './theater-layout/theater-layout.componen
MatToolbarModule, MatToolbarModule,
MatProgressBarModule, MatProgressBarModule,
MatSnackBarModule, MatSnackBarModule,
MatIconButton MatAutocompleteModule,
MatInputModule,
MatFormFieldModule,
MatIconButton,
MatDividerModule
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),

View File

@@ -6,7 +6,7 @@
</a> </a>
<div class="absolute left-1/2 transform -translate-x-1/2 text-center"> <div class="absolute left-1/2 transform -translate-x-1/2 text-center">
<h2 class="text-3xl font-bold animate-fadeUp-delay bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent"> <h2 class="text-3xl font-bold bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
Absolut war gestern, Bewegung ist heute! Absolut war gestern, Bewegung ist heute!
</h2> </h2>
</div> </div>

View File

@@ -1,4 +1,4 @@
import {Kinosaal, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; import { Kinosaal, Sitzplatz, Vorstellung, Film } 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";
@@ -67,6 +67,13 @@ export class HttpService {
} }
/* Vorstellung APIs */
/* GET /api/film */
getMovies(): Observable<Film[]> {
return this.http.get<Film[]>(`${this.baseUrl}film`);
}
/* Show-Seats APIs*/ /* Show-Seats APIs*/

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">Keine Vorstellungen gefunden</p>
<p class="text-sm">Für '{{ search() }}' finden am {{ date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric'} )}} kein Vorstellunge statt.</p>
</div>

View File

@@ -0,0 +1,12 @@
import { Component, input } from '@angular/core';
@Component({
selector: 'app-movie-schedule-no-search-result',
standalone: false,
templateUrl: './movie-schedule-no-search-result.component.html',
styleUrl: './movie-schedule-no-search-result.component.css'
})
export class MovieScheduleNoSearchResultComponent {
search = input.required<string>();
date = input.required<Date>();
}

View File

@@ -0,0 +1,16 @@
<form class="movie-search-form w-88">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Film suchen</mat-label>
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto" (click)="searchControl.setValue('')">
<!-- @if (searchControl.hasError('filmNotFound')) { -->
<!-- <mat-error>Film existiert nicht</mat-error> -->
<!-- } -->
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>

View File

@@ -0,0 +1,62 @@
import { Component, inject, OnInit, output, ViewEncapsulation } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { catchError, map, startWith, tap } from 'rxjs/operators';
import { HttpService } from '../http.service';
@Component({
selector: 'app-movie-search',
standalone: false,
templateUrl: './movie-search.component.html',
styleUrl: './movie-search.component.css',
encapsulation: ViewEncapsulation.None
})
export class MovieSearchComponent implements OnInit {
movieSearchResult = output<string>();
options: string[] = [];
filteredOptions: Observable<string[]> = new Observable();
searchControl = new FormControl('', (control) => {
if (!control.value) return null;
const value = control.value.toLowerCase();
const found = this.options.some(option =>
option.toLowerCase().includes(value)
);
return found ? null : { filmNotFound: true };
});
private http = inject(HttpService)
ngOnInit() {
this.loadMovies();
this.filteredOptions = this.searchControl.valueChanges.pipe(
startWith(''),
tap(value => this.movieSearchResult.emit(value || '')),
map(value => this._filter(value || ''))
);
}
private _filter(value: string): string[] {
const filterValue = value.toLowerCase();
return this.options.filter(option =>
option.toLowerCase().includes(filterValue)
);
}
private loadMovies() {
this.http.getMovies().pipe(
tap(movies => {
this.options = movies
.map(movie => movie.title)
.sort();
}),
catchError(err => {
console.error('Fehler beim Laden der Filme', err);
return of([]);
})
).subscribe();
}
}

View File

@@ -6,7 +6,7 @@ nav {
} }
.navbar { .navbar {
width: 200px; width: 250px;
background-color: white; background-color: white;
padding: 5px; padding: 5px;
transition: color .2s; transition: color .2s;

View File

@@ -7,7 +7,7 @@
<a [routerLink]="[item.path]" <a [routerLink]="[item.path]"
routerLinkActive="active" routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }" [routerLinkActiveOptions]="{ exact: true }"
class="text-xl font-bold flex justify-center hover:bg-gray-200 gradient-text rounded-sm"> class="text-2xl pl-3 hover:bg-gray-200 gradient-text rounded-sm">
{{ item.label }} {{ item.label }}
</a> </a>
</div> </div>

View File

@@ -12,7 +12,7 @@ export class NavbarComponent {
*/ */
navItems:{label:string, path:string}[] = [ navItems:{label:string, path:string}[] = [
{label: 'Schedule', path: '/schedule'}, {label: 'Programm', path: '/schedule'},
{label: 'Kinosaal-test', path: '/theater-overlay'}, {label: 'Kinosaal-test', path: '/theater-overlay'},
] ]
} }

View File

@@ -0,0 +1,6 @@
.schedule-header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}

View File

@@ -0,0 +1,10 @@
<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

@@ -0,0 +1,11 @@
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,10 +1,18 @@
<app-schedule-header (movieSearchResult)="movieSearchResult = $event"></app-schedule-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) {
<mat-tab [label]="dateInfo.label"> <mat-tab [label]="dateInfo.label">
@if (getMovieCount(i) > 0) { @if (getMovieCount(i) > 0) {
@if (hasSearchResults(i)) {
@for (group of dateInfo.performances; track group.movie.id) { @for (group of dateInfo.performances; track group.movie.id) {
@if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) {
<app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info> <app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info>
} }
}
} @else {
<app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date" ></app-movie-schedule-no-search-result>
}
} @else { } @else {
<app-movie-schedule-empty></app-movie-schedule-empty> <app-movie-schedule-empty></app-movie-schedule-empty>
} }

View File

@@ -16,6 +16,8 @@ export class ScheduleComponent implements OnInit {
dates: { label: string; date: Date; performances: MovieGroup[] }[] = []; dates: { label: string; date: Date; performances: MovieGroup[] }[] = [];
performaces: Vorstellung[] = []; performaces: Vorstellung[] = [];
movieSearchResult: string = '';
private http = inject(HttpService); private http = inject(HttpService);
private loading = inject(LoadingService) private loading = inject(LoadingService)
@@ -27,6 +29,14 @@ export class ScheduleComponent implements OnInit {
this.loadPerformances(); this.loadPerformances();
} }
hasSearchResults(dateIndex: number): boolean {
if (!this.movieSearchResult) return true;
return this.dates[dateIndex].performances.some(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
);
}
generateDates() { generateDates() {
const today = new Date(); const today = new Date();
for (let i = 0; i < 14; i++) { for (let i = 0; i < 14; i++) {