schedule search and optimization #6

Merged
Piet merged 2 commits from schedule into main 2025-11-05 10:59:56 +00:00
17 changed files with 174 additions and 12 deletions
Showing only changes of commit 0bd3887701 - Show all commits

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,9 +1,17 @@
<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) {
@for (group of dateInfo.performances; track group.movie.id) { @if (hasSearchResults(i)) {
<app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info> @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>
}
}
} @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++) {