schedule search and optimization #6
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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*/
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
0
src/app/movie-search/movie-search.component.css
Normal file
0
src/app/movie-search/movie-search.component.css
Normal file
16
src/app/movie-search/movie-search.component.html
Normal file
16
src/app/movie-search/movie-search.component.html
Normal 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>
|
||||||
62
src/app/movie-search/movie-search.component.ts
Normal file
62
src/app/movie-search/movie-search.component.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/app/schedule-header/schedule-header.component.css
Normal file
6
src/app/schedule-header/schedule-header.component.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.schedule-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
10
src/app/schedule-header/schedule-header.component.html
Normal file
10
src/app/schedule-header/schedule-header.component.html
Normal 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>
|
||||||
11
src/app/schedule-header/schedule-header.component.ts
Normal file
11
src/app/schedule-header/schedule-header.component.ts
Normal 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>();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user