This commit is contained in:
Constantin Simonis 2025-02-04 09:15:20 +01:00
parent 59e90dc534
commit 9ab7a128f3
Signed by: csimonis
GPG Key ID: 758DD9C506603183
19 changed files with 383 additions and 286 deletions

View File

@ -1,5 +1,4 @@
import {ErrorHandler as NgErrorHandler} from "@angular/core"; import { ErrorHandler as NgErrorHandler } from '@angular/core';
export class ErrorHandler implements NgErrorHandler { export class ErrorHandler implements NgErrorHandler {
handleError(error: any): void { handleError(error: any): void {

View File

@ -1 +0,0 @@

View File

@ -24,6 +24,8 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hotel-manager'); expect(compiled.querySelector('h1')?.textContent).toContain(
'Hello, hotel-manager',
);
}); });
}); });

View File

@ -1,16 +1,12 @@
import {Component, inject, OnInit} from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import {filter, from, map, reduce} from "rxjs"; import { filter, from, map, reduce } from 'rxjs';
import {Router, RouterOutlet} from "@angular/router"; import { Router, RouterOutlet } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [ imports: [RouterOutlet],
RouterOutlet template: ` <router-outlet /> `,
],
template: `
<router-outlet />
`
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
router: Router = inject(Router); router: Router = inject(Router);
@ -58,17 +54,24 @@ export class AppComponent implements OnInit {
{ {
name: 'Victor', name: 'Victor',
age: 39, age: 39,
} },
]; ];
from(users).pipe( from(users)
filter(user => user.age >= 18), .pipe(
reduce((acc, user) => { filter((user) => user.age >= 18),
acc.age += user.age reduce(
(acc, user) => {
acc.age += user.age;
acc.count++; acc.count++;
return acc; return acc;
}, {age: 0, count: 0}), },
map(data => data.age / data.count), { age: 0, count: 0 },
).subscribe((data) => {console.log("avg age: ", data)}); ),
map((data) => data.age / data.count),
)
.subscribe((data) => {
console.log('avg age: ', data);
});
} }
} }

View File

@ -1,19 +1,23 @@
import {ApplicationConfig, importProvidersFrom, provideZoneChangeDetection} from '@angular/core'; import {
ApplicationConfig,
importProvidersFrom,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import {registerLocaleData} from "@angular/common"; import { registerLocaleData } from '@angular/common';
import localeDe from "@angular/common/locales/de" import localeDe from '@angular/common/locales/de';
import localeCn from "@angular/common/locales/en" import localeCn from '@angular/common/locales/en';
import localeJap from "@angular/common/locales/en" import localeJap from '@angular/common/locales/en';
import {provideHttpClient} from "@angular/common/http"; import { provideHttpClient } from '@angular/common/http';
import {InMemoryWebApiModule} from "angular-in-memory-web-api"; import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import {HotelDataService} from "./hotel/service/HotelData.service"; import { HotelDataService } from './hotel/service/HotelData.service';
import {ErrorHandler} from "./ErrorHandler"; import { ErrorHandler } from './ErrorHandler';
import {ErrorHandler as NgErrorHandler} from "@angular/core"; import { ErrorHandler as NgErrorHandler } from '@angular/core';
registerLocaleData(localeDe, 'de-DE') registerLocaleData(localeDe, 'de-DE');
registerLocaleData(localeCn, 'cn-CN') registerLocaleData(localeCn, 'cn-CN');
registerLocaleData(localeJap, 'ja-JP') registerLocaleData(localeJap, 'ja-JP');
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
@ -22,7 +26,7 @@ export const appConfig: ApplicationConfig = {
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelDataService)), importProvidersFrom(InMemoryWebApiModule.forRoot(HotelDataService)),
{ {
provide: NgErrorHandler, provide: NgErrorHandler,
useClass: ErrorHandler useClass: ErrorHandler,
} },
] ],
}; };

View File

@ -1,7 +1,7 @@
import {Routes} from '@angular/router'; import { Routes } from '@angular/router';
import {HotelsComponent} from "./hotel/component/hotels.component"; import { HotelsComponent } from './hotel/component/hotels.component';
import {HotelComponent} from "./hotel/component/hotel.component"; import { HotelComponent } from './hotel/component/hotel.component';
import {CreateHotelComponent} from "./hotel/component/create-hotel.component"; import { CreateHotelComponent } from './hotel/component/create-hotel.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -12,11 +12,11 @@ export const routes: Routes = [
{ {
path: 'hotels/new', path: 'hotels/new',
component: CreateHotelComponent, component: CreateHotelComponent,
title: 'New Hotel' title: 'New Hotel',
}, },
{ {
path: 'hotels/:hotelId', path: 'hotels/:hotelId',
component: HotelComponent, component: HotelComponent,
title: 'Hotel', title: 'Hotel',
} },
]; ];

View File

@ -1,25 +1,20 @@
import {Component, EventEmitter, Output} from "@angular/core"; import { Component, EventEmitter, Output } from '@angular/core';
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {Lang} from "./lang"; import { Lang } from './lang';
@Component({ @Component({
standalone: true, standalone: true,
selector: "app-currency", selector: 'app-currency',
imports: [ imports: [ReactiveFormsModule, FormsModule],
ReactiveFormsModule,
FormsModule
],
template: ` template: `
<select [ngModel]="currency" (ngModelChange)="setCurrency($event)"> <select [ngModel]="currency" (ngModelChange)="setCurrency($event)">
@for (currency of currencies; track currency.code) { @for (currency of currencies; track currency.code) {
<option value="{{currency.name}}">{{ currency.name }}</option> <option value="{{ currency.name }}">{{ currency.name }}</option>
} }
</select> </select>
` `,
}) })
export class CurrencyComponent { export class CurrencyComponent {
@Output() @Output()
public currency: EventEmitter<Lang> = new EventEmitter(); public currency: EventEmitter<Lang> = new EventEmitter();
@ -27,24 +22,24 @@ export class CurrencyComponent {
{ {
name: 'de', name: 'de',
code: 'de-DE', code: 'de-DE',
currency: 'EUR' currency: 'EUR',
}, },
{ {
name: 'en', name: 'en',
code: 'en-US', code: 'en-US',
currency: 'USD' currency: 'USD',
}, },
{ {
name: 'jap', name: 'jap',
code: 'ja-JP', code: 'ja-JP',
currency: 'JPY' currency: 'JPY',
}, },
{ {
name: 'cn', name: 'cn',
code: 'cn-CN', code: 'cn-CN',
currency: 'CNY' currency: 'CNY',
} },
] ];
public setCurrency(currencyInput: string): void { public setCurrency(currencyInput: string): void {
for (const currency of this.currencies) { for (const currency of this.currencies) {

View File

@ -1,22 +1,18 @@
import {Component, inject} from "@angular/core"; import { Component, inject } from '@angular/core';
import {Hotel} from "../model/hotel"; import { Hotel } from '../model/hotel';
import {HotelService} from "../service/hotel.service"; import { HotelService } from '../service/hotel.service';
import {EditHotelComponent} from "./edit-hotel.component"; import { EditHotelComponent } from './edit-hotel.component';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
@Component({ @Component({
selector: "app-create-hotel", selector: 'app-create-hotel',
standalone: true, standalone: true,
imports: [ imports: [EditHotelComponent],
EditHotelComponent
],
template: ` template: `
<app-edit-hotel (updateHotel)="create($event)"></app-edit-hotel> <app-edit-hotel (updateHotel)="create($event)"></app-edit-hotel>
` `,
}) })
export class CreateHotelComponent { export class CreateHotelComponent {
hotelService: HotelService = inject(HotelService); hotelService: HotelService = inject(HotelService);
router: Router = inject(Router); router: Router = inject(Router);

View File

@ -1,14 +1,11 @@
import {Component, inject, Input} from "@angular/core"; import { Component, inject, Input } from '@angular/core';
import {HotelService} from "../service/hotel.service"; import { HotelService } from '../service/hotel.service';
import {Router} from "@angular/router"; import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-delete-hotel', selector: 'app-delete-hotel',
standalone: true, standalone: true,
template: ` template: ` <button (click)="delete()">Delete Hotel</button> `,
<button (click)="delete()">Delete Hotel</button>
`
}) })
export class DeleteHotelComponent { export class DeleteHotelComponent {
@Input() @Input()
@ -20,7 +17,7 @@ export class DeleteHotelComponent {
delete() { delete() {
if (confirm('Are you sure?')) { if (confirm('Are you sure?')) {
this.hotelService.deleteById(this.id).subscribe() this.hotelService.deleteById(this.id).subscribe();
this.router.navigate(['/hotels']); this.router.navigate(['/hotels']);
} }
} }

View File

@ -1,4 +1,11 @@
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { import {
AbstractControl, AbstractControl,
FormArray, FormArray,
@ -7,13 +14,13 @@ import {
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ValidationErrors, ValidationErrors,
Validators Validators,
} from "@angular/forms"; } from '@angular/forms';
import {Hotel} from "../model/hotel"; import { Hotel } from '../model/hotel';
import {RouterLink} from "@angular/router"; import { RouterLink } from '@angular/router';
import {NgForOf, NgIf} from "@angular/common"; import { NgForOf, NgIf } from '@angular/common';
import {debounceTime} from "rxjs"; import { debounceTime } from 'rxjs';
import {ErrorMsgComponent} from "./error-msg.component"; import { ErrorMsgComponent } from './error-msg.component';
@Component({ @Component({
selector: 'app-edit-hotel', selector: 'app-edit-hotel',
@ -29,83 +36,101 @@ import {ErrorMsgComponent} from "./error-msg.component";
template: ` template: `
<ng-form [formGroup]="form" (ngSubmit)="submit()"> <ng-form [formGroup]="form" (ngSubmit)="submit()">
<label for="name">Name</label> <label for="name">Name</label>
<br> <br />
<input id="name" type="text" formControlName="name"> <input id="name" type="text" formControlName="name" />
<app-error-msg [msg]="errorMsgs['name']"/> <app-error-msg [msg]="errorMsgs['name']" />
<br> <br />
<label for="description">Description</label> <label for="description">Description</label>
<br> <br />
<input id="description" type="text" formControlName="description"> <input id="description" type="text" formControlName="description" />
<app-error-msg [msg]="errorMsgs['description']"/> <app-error-msg [msg]="errorMsgs['description']" />
<br> <br />
<label for="price">Price</label> <label for="price">Price</label>
<br> <br />
<input id="price" type="number" formControlName="price"> <input id="price" type="number" formControlName="price" />
<app-error-msg [msg]="errorMsgs['price']"/> <app-error-msg [msg]="errorMsgs['price']" />
<br> <br />
<label for="rating">Rating</label> <label for="rating">Rating</label>
<br> <br />
<input id="rating" type="number" formControlName="rating"> <input id="rating" type="number" formControlName="rating" />
<app-error-msg [msg]="errorMsgs['rating']"/> <app-error-msg [msg]="errorMsgs['rating']" />
<br> <br />
<br> <br />
<label for="email">Email</label> <label for="email">Email</label>
<input id="email" type="checkbox" name="contactType" value="Email" [checked]="activeContact == 'email'" <input
(click)="changeContactType('email')"> id="email"
<br> type="checkbox"
name="contactType"
value="Email"
[checked]="activeContact == 'email'"
(click)="changeContactType('email')"
/>
<br />
<label for="phoneNumber">Phone Number</label> <label for="phoneNumber">Phone Number</label>
<input id="phoneNumber" type="checkbox" name="contactType" value="PhoneNumber" <input
[checked]="activeContact == 'phoneNumber'" (click)="changeContactType('phoneNumber')"> id="phoneNumber"
<br> type="checkbox"
name="contactType"
value="PhoneNumber"
[checked]="activeContact == 'phoneNumber'"
(click)="changeContactType('phoneNumber')"
/>
<br />
<label for="none">None</label> <label for="none">None</label>
<input id="none" type="checkbox" name="contactType" value="none" [checked]="activeContact == 'none'" <input
(click)="changeContactType('none')"> id="none"
type="checkbox"
name="contactType"
value="none"
[checked]="activeContact == 'none'"
(click)="changeContactType('none')"
/>
<br> <br />
<br> <br />
@if (activeContact === 'email') { @if (activeContact === 'email') {
<app-error-msg [msg]="errorMsgs['emailGroup']"/> <app-error-msg [msg]="errorMsgs['emailGroup']" />
<div formGroupName="emailGroup"> <div formGroupName="emailGroup">
<app-error-msg [msg]="errorMsgs['email']"/> <app-error-msg [msg]="errorMsgs['email']" />
<br> <br />
<label for="email">Email</label> <label for="email">Email</label>
<br> <br />
<input type="email" formControlName="email" required> <input type="email" formControlName="email" required />
<br> <br />
<label for="emailConfirm">Confirm Email</label> <label for="emailConfirm">Confirm Email</label>
<app-error-msg [msg]="errorMsgs['emailConfirm']"/> <app-error-msg [msg]="errorMsgs['emailConfirm']" />
<br> <br />
<input type="email" formControlName="emailConfirm" required> <input type="email" formControlName="emailConfirm" required />
</div> </div>
} @else if (activeContact === 'phoneNumber') { } @else if (activeContact === 'phoneNumber') {
<app-error-msg [msg]="errorMsgs['phoneNumberGroup']"/> <app-error-msg [msg]="errorMsgs['phoneNumberGroup']" />
<div formGroupName="phoneNumberGroup"> <div formGroupName="phoneNumberGroup">
<app-error-msg [msg]="errorMsgs['phoneNumber']"/> <app-error-msg [msg]="errorMsgs['phoneNumber']" />
<br> <br />
<label for="phoneNumber">Phone Number</label> <label for="phoneNumber">Phone Number</label>
<br> <br />
<input type="number" formControlName="phoneNumber" required> <input type="number" formControlName="phoneNumber" required />
<br> <br />
<label for="phoneNumberConfirm">Confirm Phone Number</label> <label for="phoneNumberConfirm">Confirm Phone Number</label>
<app-error-msg [msg]="errorMsgs['phoneNumberConfirm']"/> <app-error-msg [msg]="errorMsgs['phoneNumberConfirm']" />
<br> <br />
<input type="number" formControlName="phoneNumberConfirm" required> <input type="number" formControlName="phoneNumberConfirm" required />
</div> </div>
} }
<br> <br />
<br> <br />
<button (click)="addTag()">+</button> <button (click)="addTag()">+</button>
<div formArrayName="tags"> <div formArrayName="tags">
<div *ngFor="let tag of getTags().controls; let i = index"> <div *ngFor="let tag of getTags().controls; let i = index">
<input type="text" placeholder="tag" [formControlName]="i"> <input type="text" placeholder="tag" [formControlName]="i" />
<button (click)="deleteTag(tag)">-</button> <button (click)="deleteTag(tag)">-</button>
</div> </div>
<app-error-msg [msg]="errorMsgs['tags']"/> <app-error-msg [msg]="errorMsgs['tags']" />
</div> </div>
<br> <br />
<br> <br />
<button type="submit" (click)="submit()">Submit</button> <button type="submit" (click)="submit()">Submit</button>
</ng-form> </ng-form>
@ -119,9 +144,9 @@ export class EditHotelComponent implements OnInit {
hotel: Hotel | null = null; hotel: Hotel | null = null;
@Output() @Output()
updateHotel: EventEmitter<Hotel> = new EventEmitter updateHotel: EventEmitter<Hotel> = new EventEmitter();
form!: FormGroup form!: FormGroup;
validationErrors: Record<string, string> = { validationErrors: Record<string, string> = {
required: 'This field is required', required: 'This field is required',
@ -154,13 +179,19 @@ export class EditHotelComponent implements OnInit {
this.form = new FormGroup({ this.form = new FormGroup({
name: new FormControl(this.hotel?.hotelName, [Validators.required]), name: new FormControl(this.hotel?.hotelName, [Validators.required]),
description: new FormControl(this.hotel?.description, [Validators.required]), description: new FormControl(this.hotel?.description, [
Validators.required,
]),
price: new FormControl(this.hotel?.price, [Validators.required]), price: new FormControl(this.hotel?.price, [Validators.required]),
rating: new FormControl(this.hotel?.rating, [Validators.required, Validators.min(0), Validators.max(5)]), rating: new FormControl(this.hotel?.rating, [
Validators.required,
Validators.min(0),
Validators.max(5),
]),
tags: new FormArray(tags), tags: new FormArray(tags),
}); });
this.initGroups() this.initGroups();
if (this.hotel?.email) { if (this.hotel?.email) {
this.form.addControl('emailGroup', this.emailGroup); this.form.addControl('emailGroup', this.emailGroup);
@ -168,7 +199,7 @@ export class EditHotelComponent implements OnInit {
this.form.addControl('phoneNumberGroup', this.phoneNumberGroup); this.form.addControl('phoneNumberGroup', this.phoneNumberGroup);
} }
Object.keys(this.form.controls).forEach(controlName => { Object.keys(this.form.controls).forEach((controlName) => {
let debounce = 1000; let debounce = 1000;
const control = this.form.get(controlName); const control = this.form.get(controlName);
if (!control) { if (!control) {
@ -180,7 +211,7 @@ export class EditHotelComponent implements OnInit {
} }
control?.valueChanges?.pipe(debounceTime(debounce)).subscribe(() => { control?.valueChanges?.pipe(debounceTime(debounce)).subscribe(() => {
this.setErrorMessage(controlName, control) this.setErrorMessage(controlName, control);
}); });
}); });
} }
@ -192,7 +223,7 @@ export class EditHotelComponent implements OnInit {
} }
const hotel: Hotel = { const hotel: Hotel = {
imageUrl: this.hotel?.imageUrl ?? "", imageUrl: this.hotel?.imageUrl ?? '',
hotelName: this.form.value.name, hotelName: this.form.value.name,
description: this.form.value.description, description: this.form.value.description,
price: this.form.value.price, price: this.form.value.price,
@ -200,10 +231,10 @@ export class EditHotelComponent implements OnInit {
id: this.hotel?.id ?? 0, id: this.hotel?.id ?? 0,
tags: this.form.value.tags ?? [], tags: this.form.value.tags ?? [],
email: this.form.value.email, email: this.form.value.email,
phoneNumber: this.form.value.phoneNumber phoneNumber: this.form.value.phoneNumber,
}; };
this.updateHotel.emit(hotel) this.updateHotel.emit(hotel);
this.hotel = hotel; this.hotel = hotel;
} }
@ -221,11 +252,13 @@ export class EditHotelComponent implements OnInit {
} }
setErrorMessage(controlName: string, control: AbstractControl) { setErrorMessage(controlName: string, control: AbstractControl) {
this.errorMsgs[controlName] = Object.keys(control.errors ?? {}).map((key) => this.validationErrors[key]).join(' '); this.errorMsgs[controlName] = Object.keys(control.errors ?? {})
.map((key) => this.validationErrors[key])
.join(' ');
} }
showErrors() { showErrors() {
Object.keys(this.form.controls).forEach(controlName => { Object.keys(this.form.controls).forEach((controlName) => {
const control = this.form.get(controlName); const control = this.form.get(controlName);
if (!control) { if (!control) {
return; return;
@ -254,7 +287,7 @@ export class EditHotelComponent implements OnInit {
} }
if (email.value !== emailConfirm.value) { if (email.value !== emailConfirm.value) {
return {emailMatch: true}; return { emailMatch: true };
} }
return null; return null;
@ -269,21 +302,41 @@ export class EditHotelComponent implements OnInit {
} }
if (phoneNumber.value !== phoneNumberConfirm.value) { if (phoneNumber.value !== phoneNumberConfirm.value) {
return {phoneNumberMatch: true}; return { phoneNumberMatch: true };
} }
return null; return null;
} }
private initGroups() { private initGroups() {
this.emailGroup = new FormGroup({ this.emailGroup = new FormGroup(
email: new FormControl(this.hotel?.email, [Validators.required, Validators.email]), {
emailConfirm: new FormControl(this.hotel?.email, [Validators.required, Validators.email]), email: new FormControl(this.hotel?.email, [
}, {validators: this.emailMatch}); Validators.required,
Validators.email,
]),
emailConfirm: new FormControl(this.hotel?.email, [
Validators.required,
Validators.email,
]),
},
{ validators: this.emailMatch },
);
this.phoneNumberGroup = new FormGroup({ this.phoneNumberGroup = new FormGroup(
phoneNumber: new FormControl(this.hotel?.phoneNumber, [Validators.required, Validators.maxLength(10), Validators.minLength(10)]), {
phoneNumberConfirm: new FormControl(this.hotel?.phoneNumber, [Validators.required, Validators.maxLength(10), Validators.minLength(10)]), phoneNumber: new FormControl(this.hotel?.phoneNumber, [
}, {validators: this.phoneNumberMatch}); Validators.required,
Validators.maxLength(10),
Validators.minLength(10),
]),
phoneNumberConfirm: new FormControl(this.hotel?.phoneNumber, [
Validators.required,
Validators.maxLength(10),
Validators.minLength(10),
]),
},
{ validators: this.phoneNumberMatch },
);
} }
} }

View File

@ -1,13 +1,10 @@
import {Component, Input} from "@angular/core"; import { Component, Input } from '@angular/core';
import {NgIf} from "@angular/common"; import { NgIf } from '@angular/common';
@Component({ @Component({
standalone: true, standalone: true,
selector: 'app-error-msg', selector: 'app-error-msg',
imports: [ imports: [NgIf],
NgIf
],
template: ` template: `
<div *ngIf="msg" class="alert alert-danger border"> <div *ngIf="msg" class="alert alert-danger border">
{{ msg }} {{ msg }}
@ -38,7 +35,7 @@ import {NgIf} from "@angular/common";
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
` `,
}) })
export class ErrorMsgComponent { export class ErrorMsgComponent {
@Input() @Input()

View File

@ -1,13 +1,12 @@
import {Component, inject, OnInit} from "@angular/core"; import { Component, inject, OnInit } from '@angular/core';
import {Hotel} from "../model/hotel" import { Hotel } from '../model/hotel';
import {CurrencyPipe, NgOptimizedImage} from "@angular/common"; import { CurrencyPipe, NgOptimizedImage } from '@angular/common';
import {StarComponent} from "./star.component"; import { StarComponent } from './star.component';
import {ActivatedRoute} from "@angular/router"; import { ActivatedRoute } from '@angular/router';
import {HotelService} from "../service/hotel.service"; import { HotelService } from '../service/hotel.service';
import {catchError, EMPTY} from "rxjs"; import { catchError, EMPTY } from 'rxjs';
import {EditHotelComponent} from "./edit-hotel.component"; import { EditHotelComponent } from './edit-hotel.component';
import {DeleteHotelComponent} from "./delete-hotel.component"; import { DeleteHotelComponent } from './delete-hotel.component';
@Component({ @Component({
standalone: true, standalone: true,
@ -17,26 +16,25 @@ import {DeleteHotelComponent} from "./delete-hotel.component";
StarComponent, StarComponent,
NgOptimizedImage, NgOptimizedImage,
EditHotelComponent, EditHotelComponent,
DeleteHotelComponent DeleteHotelComponent,
], ],
template: ` template: `
<div style="border: white 2px; border-radius: 2px"> <div style="border: white 2px; border-radius: 2px">
@if (hotel && !alert) { @if (hotel && !alert) {
<app-edit-hotel [hotel]="hotel" (updateHotel)="update($event)" /> <app-edit-hotel [hotel]="hotel" (updateHotel)="update($event)" />
<app-delete-hotel [id]="hotel.id" /> <app-delete-hotel [id]="hotel.id" />
} @else if(alert) { } @else if (alert) {
<h2>{{alert}}</h2> <h2>{{ alert }}</h2>
} @else { } @else {
<h2>loading</h2> <h2>loading</h2>
} }
</div> </div>
` `,
}) })
export class HotelComponent implements OnInit { export class HotelComponent implements OnInit {
protected hotel!: Hotel; protected hotel!: Hotel;
protected alert: string|undefined protected alert: string | undefined;
private route: ActivatedRoute = inject(ActivatedRoute); private route: ActivatedRoute = inject(ActivatedRoute);
@ -44,15 +42,22 @@ export class HotelComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
const hotelId = this.route.snapshot.params['hotelId']; const hotelId = this.route.snapshot.params['hotelId'];
this.hotelService.getHotelById(hotelId) this.hotelService
.pipe(catchError((err) => { .getHotelById(hotelId)
this.alert = `Hotel could not be retrieved: ${err.message}` .pipe(
catchError((err) => {
this.alert = `Hotel could not be retrieved: ${err.message}`;
return EMPTY; return EMPTY;
})) }),
.subscribe({next: (hotel: Hotel) => {this.hotel = hotel}}) )
.subscribe({
next: (hotel: Hotel) => {
this.hotel = hotel;
},
});
} }
update(hotel: Hotel): void { update(hotel: Hotel): void {
this.hotelService.updateHotelById(hotel).subscribe() this.hotelService.updateHotelById(hotel).subscribe();
} }
} }

View File

@ -1,52 +1,67 @@
import {ChangeDetectionStrategy, Component, inject} from "@angular/core"; import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import {HotelComponent} from "./hotel.component"; import { HotelComponent } from './hotel.component';
import {Hotel} from "../model/hotel"; import { Hotel } from '../model/hotel';
import {FormsModule} from "@angular/forms"; import { FormsModule } from '@angular/forms';
import {Lang} from "../../currency/lang"; import { Lang } from '../../currency/lang';
import {HotelService} from "../service/hotel.service"; import { HotelService } from '../service/hotel.service';
import {Observable} from "rxjs"; import { Observable } from 'rxjs';
import {AsyncPipe, NgIf} from "@angular/common"; import { AsyncPipe, NgIf } from '@angular/common';
import {CurrencyComponent} from "../../currency/currency.component"; import { CurrencyComponent } from '../../currency/currency.component';
import {RouterLink} from "@angular/router"; import { RouterLink } from '@angular/router';
import {StarComponent} from "./star.component"; import { StarComponent } from './star.component';
@Component({ @Component({
standalone: true, standalone: true,
template: ` template: `
<app-currency (currency)="currency = $event"></app-currency> <app-currency (currency)="currency = $event"></app-currency>
<form> <form>
<input name="search" [ngModel]="search" (ngModelChange)="searchEvent($event)"> <input
name="search"
[ngModel]="search"
(ngModelChange)="searchEvent($event)"
/>
</form> </form>
<p routerLink="/hotels/new">Create Hotel</p> <p routerLink="/hotels/new">Create Hotel</p>
@for (hotel of (matchingHotels | async); track hotel.id) { @for (hotel of matchingHotels | async; track hotel.id) {
<div *ngIf="hotel.hotelName.toLowerCase().includes(search)"> <div *ngIf="hotel.hotelName.toLowerCase().includes(search)">
<div>{{ hotel.hotelName }}</div> <div>{{ hotel.hotelName }}</div>
<img src="{{hotel.imageUrl}}" alt="{{hotel.hotelName}}" height="64" width="64"> <img
<br> src="{{ hotel.imageUrl }}"
<button routerLink="{{hotel.id}}">Details</button> alt="{{ hotel.hotelName }}"
<hr> height="64"
width="64"
/>
<br />
<button routerLink="{{ hotel.id }}">Details</button>
<hr />
</div> </div>
} @empty { } @empty {
<h1>no matching results for {{ search }}</h1> <h1>no matching results for {{ search }}</h1>
} }
`, `,
imports: [FormsModule, HotelComponent, AsyncPipe, CurrencyComponent, RouterLink, StarComponent, NgIf], imports: [
FormsModule,
HotelComponent,
AsyncPipe,
CurrencyComponent,
RouterLink,
StarComponent,
NgIf,
],
providers: [HotelService], providers: [HotelService],
selector: 'app-hotels', selector: 'app-hotels',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class HotelsComponent { export class HotelsComponent {
public currency: Lang = { name: 'de', code: 'de-DE', currency: 'EUR' };
public currency: Lang = {name: 'de', code: 'de-DE', currency: 'EUR'};
public search: string = ''; public search: string = '';
private hotelService: HotelService= inject(HotelService); private hotelService: HotelService = inject(HotelService);
public matchingHotels: Observable<Hotel[]> = this.hotelService.getHotels(); public matchingHotels: Observable<Hotel[]> = this.hotelService.getHotels();
public searchEvent(input: string) { public searchEvent(input: string) {
this.search = input.toLowerCase(); this.search = input.toLowerCase();
} }
} }

View File

@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core'; import { Component, Input } from '@angular/core';
@Component({ @Component({
selector: 'app-star', selector: 'app-star',
@ -6,10 +6,32 @@ import {Component, Input} from '@angular/core';
imports: [], imports: [],
template: ` template: `
@for (_ of getList(); track null) { @for (_ of getList(); track null) {
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" color="yellow"><path fill="currentColor" d="m5.825 21l1.625-7.025L2 9.25l7.2-.625L12 2l2.8 6.625l7.2.625l-5.45 4.725L18.175 21L12 17.275z"/></svg> <svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
color="yellow"
>
<path
fill="currentColor"
d="m5.825 21l1.625-7.025L2 9.25l7.2-.625L12 2l2.8 6.625l7.2.625l-5.45 4.725L18.175 21L12 17.275z"
/>
</svg>
} }
@if (rating % 1 >= 0.5) { @if (rating % 1 >= 0.5) {
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" color="yellow"><path fill="currentColor" d="M12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21L12 17.27z"/></svg> <svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
color="yellow"
>
<path
fill="currentColor"
d="M12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21L12 17.27z"
/>
</svg>
} }
`, `,
}) })

View File

@ -2,8 +2,8 @@ export interface Hotel {
id: number; id: number;
hotelName: string; hotelName: string;
description: string; description: string;
email: string|null, email: string | null;
phoneNumber: string|null, phoneNumber: string | null;
price: number; price: number;
imageUrl: string; imageUrl: string;
rating: number; rating: number;

View File

@ -1,57 +1,55 @@
import {InMemoryDbService} from "angular-in-memory-web-api"; import { InMemoryDbService } from 'angular-in-memory-web-api';
import {Hotel} from "../model/hotel"; import { Hotel } from '../model/hotel';
export class HotelDataService implements InMemoryDbService {
export class HotelDataService implements InMemoryDbService{
createDb(): Record<string, Hotel[]> { createDb(): Record<string, Hotel[]> {
const hotels: Hotel[] = [ const hotels: Hotel[] = [
{ {
"id": 1, id: 1,
"hotelName": "Buea süßes Leben", hotelName: 'Buea süßes Leben',
"description": "Schöne Aussicht am Meer", description: 'Schöne Aussicht am Meer',
"price": 230.5, price: 230.5,
"imageUrl": "assets/img/heisenberg.jpg", imageUrl: 'assets/img/heisenberg.jpg',
"rating": 3.5, rating: 3.5,
"tags": ["Meer", "Berge"], tags: ['Meer', 'Berge'],
"email": "buea@mail.com", email: 'buea@mail.com',
"phoneNumber": "1234567890" phoneNumber: '1234567890',
}, },
{ {
"id": 2, id: 2,
"hotelName": "Marrakesch", hotelName: 'Marrakesch',
"description": "Genießen Sie den Blick auf die Berge", description: 'Genießen Sie den Blick auf die Berge',
"price": 145.5, price: 145.5,
"imageUrl": "assets/img/kjan.png", imageUrl: 'assets/img/kjan.png',
"rating": 5, rating: 5,
"tags": ["Meer", "Berge"], tags: ['Meer', 'Berge'],
"email": "marrakesch@mail.com", email: 'marrakesch@mail.com',
"phoneNumber": "1234567890" phoneNumber: '1234567890',
}, },
{ {
"id": 3, id: 3,
"hotelName": "Abuja neuer Palast", hotelName: 'Abuja neuer Palast',
"description": "Kompletter Aufenthalt mit Autoservice", description: 'Kompletter Aufenthalt mit Autoservice',
"price": 120.12, price: 120.12,
"imageUrl": "assets/img/huy.png", imageUrl: 'assets/img/huy.png',
"rating": 4, rating: 4,
"tags": ["Meer", "Berge"], tags: ['Meer', 'Berge'],
"email": "abuja@mail.com", email: 'abuja@mail.com',
"phoneNumber": "1234567890" phoneNumber: '1234567890',
}, },
{ {
"id": 4, id: 4,
"hotelName": "OUR Hotel", hotelName: 'OUR Hotel',
"description": "Wunderschönes Ambiente für Ihren Aufenthalt", description: 'Wunderschönes Ambiente für Ihren Aufenthalt',
"price": 135.12, price: 135.12,
"imageUrl": "assets/img/rat.png", imageUrl: 'assets/img/rat.png',
"rating": 2.5, rating: 2.5,
"tags": ["Meer", "Berge"], tags: ['Meer', 'Berge'],
"email": "our@mail.com", email: 'our@mail.com',
"phoneNumber": "1234567890" phoneNumber: '1234567890',
} },
]; ];
return { hotels }; return { hotels };
} }
} }

View File

@ -1,10 +1,10 @@
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, shareReplay} from "rxjs"; import { Observable, shareReplay } from 'rxjs';
import {Hotel} from "../model/hotel"; import { Hotel } from '../model/hotel';
@Injectable({ @Injectable({
providedIn: "root", providedIn: 'root',
}) })
export class HotelService { export class HotelService {
private httpClient: HttpClient = inject(HttpClient); private httpClient: HttpClient = inject(HttpClient);
@ -18,11 +18,11 @@ export class HotelService {
} }
public updateHotelById(hotel: Hotel): Observable<Object> { public updateHotelById(hotel: Hotel): Observable<Object> {
return this.httpClient.put<Hotel>(`/api/hotels/${hotel.id}`, hotel) return this.httpClient.put<Hotel>(`/api/hotels/${hotel.id}`, hotel);
} }
public createHotel(hotel: Hotel): Observable<Hotel> { public createHotel(hotel: Hotel): Observable<Hotel> {
return this.httpClient.post<Hotel>(`/api/hotels`, hotel) return this.httpClient.post<Hotel>(`/api/hotels`, hotel);
} }
public deleteById(id: number): Observable<Hotel> { public deleteById(id: number): Observable<Hotel> {

View File

@ -1,15 +1,26 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>HotelManager</title> <title>HotelManager</title>
<base href="/"> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js" integrity="sha512-ykZ1QQr0Jy/4ZkvKuqWn4iF3lqPZyij9iRv6sGqLRdTPkY69YX6+7wvVGmsdBbiIfN/8OdsI7HABjvEok6ZopQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap-grid.min.css" integrity="sha512-i1b/nzkVo97VN5WbEtaPebBG8REvjWeqNclJ6AItj7msdVcaveKrlIIByDpvjk5nwHjXkIqGZscVxOrTb9tsMA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"
<link rel="icon" type="image/x-icon" href="favicon.ico"> integrity="sha512-ykZ1QQr0Jy/4ZkvKuqWn4iF3lqPZyij9iRv6sGqLRdTPkY69YX6+7wvVGmsdBbiIfN/8OdsI7HABjvEok6ZopQ=="
</head> crossorigin="anonymous"
<body> referrerpolicy="no-referrer"
></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap-grid.min.css"
integrity="sha512-i1b/nzkVo97VN5WbEtaPebBG8REvjWeqNclJ6AItj7msdVcaveKrlIIByDpvjk5nwHjXkIqGZscVxOrTb9tsMA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@ -2,5 +2,6 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component'; import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig) bootstrapApplication(AppComponent, appConfig).catch((err) =>
.catch((err) => console.error(err)); console.error(err),
);