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