Compare commits
5 Commits
main
...
jleibl_hot
Author | SHA1 | Date | |
---|---|---|---|
|
6d520d4467 | ||
|
a6829fc3f5 | ||
|
6e5eadd3a2 | ||
|
074da034c5 | ||
|
dd4faea712 |
@ -28,6 +28,7 @@
|
|||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
@ -86,6 +87,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||||
"src/styles.css"
|
"src/styles.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
|
5928
package-lock.json
generated
5928
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@ -10,23 +10,25 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.0",
|
"@angular/animations": "^18.2.3",
|
||||||
"@angular/common": "^19.0.0",
|
"@angular/cdk": "^18.2.3",
|
||||||
"@angular/compiler": "^19.0.0",
|
"@angular/common": "^18.2.3",
|
||||||
"@angular/core": "^19.0.0",
|
"@angular/compiler": "^18.2.3",
|
||||||
"@angular/forms": "^19.0.0",
|
"@angular/core": "^18.2.3",
|
||||||
"@angular/platform-browser": "^19.0.0",
|
"@angular/forms": "^18.2.3",
|
||||||
"@angular/platform-browser-dynamic": "^19.0.0",
|
"@angular/material": "^18.2.3",
|
||||||
"@angular/router": "^19.0.0",
|
"@angular/platform-browser": "^18.2.3",
|
||||||
|
"@angular/platform-browser-dynamic": "^18.2.3",
|
||||||
|
"@angular/router": "^18.2.3",
|
||||||
"angular-in-memory-web-api": "^0.18.0",
|
"angular-in-memory-web-api": "^0.18.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.14.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.0.2",
|
"@angular-devkit/build-angular": "^18.2.3",
|
||||||
"@angular/cli": "^19.0.2",
|
"@angular/cli": "^18.2.3",
|
||||||
"@angular/compiler-cli": "^19.0.0",
|
"@angular/compiler-cli": "^18.2.3",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
@ -39,4 +41,4 @@
|
|||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "~5.5.2"
|
"typescript": "~5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1 +0,0 @@
|
|||||||
<button type="button" class="px-6 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">{{ text }}</button>
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-button',
|
|
||||||
standalone: true,
|
|
||||||
templateUrl: './button.component.html',
|
|
||||||
})
|
|
||||||
export class ButtonComponent {
|
|
||||||
public text = 'Test';
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
<h1 class="text-2xl font-bold mb-4">Child</h1>
|
|
||||||
<p class="text-lg mb-2">Balance: {{balance}}</p>
|
|
||||||
<p [class.hidden]="!isBroke" class="text-red-500 mb-4">I'm broke. Now I'm about as poor as Jan-Marlon.</p>
|
|
||||||
<button type="button" (click)="takeMoney()" class="px-6 py-2 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">Take Money</button>
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Component, Input, Output, EventEmitter } from "@angular/core";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-child',
|
|
||||||
standalone: true,
|
|
||||||
templateUrl: './child.component.html',
|
|
||||||
})
|
|
||||||
export class ChildComponent {
|
|
||||||
@Input() public balance: number = 0;
|
|
||||||
@Output() balanceChange = new EventEmitter<number>();
|
|
||||||
|
|
||||||
public isBroke = false;
|
|
||||||
|
|
||||||
public takeMoney() {
|
|
||||||
if (this.balance <= 0) {
|
|
||||||
this.isBroke = true;
|
|
||||||
} else {
|
|
||||||
this.balance -= 50;
|
|
||||||
if (this.balance == 0) {
|
|
||||||
this.isBroke = true;
|
|
||||||
}
|
|
||||||
this.balanceChange.emit(this.balance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
<div class="m-3">
|
|
||||||
<h1>Parent</h1>
|
|
||||||
<p>Kontostand: {{ balance }}</p>
|
|
||||||
<button type="button" class="button" (click)=addFifty()>Add Money</button>
|
|
||||||
|
|
||||||
<app-child [(balance)]="balance"></app-child>
|
|
||||||
<app-child [(balance)]="balance"></app-child>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
|||||||
import { Component } from "@angular/core";
|
|
||||||
import { ChildComponent } from "../Child/child.component";
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-parent',
|
|
||||||
standalone: true,
|
|
||||||
templateUrl: './parent.component.html',
|
|
||||||
imports: [ChildComponent],
|
|
||||||
})
|
|
||||||
export class ParentComponent {
|
|
||||||
public balance = 1000;
|
|
||||||
|
|
||||||
public addFifty() {
|
|
||||||
this.balance += 50;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
import { Injectable } from "@angular/core";
|
|
||||||
import { Hotel } from "../../HotelItem/hotel";
|
|
||||||
import { from, Observable } from "rxjs";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HotelService {
|
|
||||||
public getHotels(): Observable<Hotel> {
|
|
||||||
return from([
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"hotelName": "Buea süßes Leben",
|
|
||||||
"description": "Schöne Aussicht am Meer",
|
|
||||||
"price": 230.5,
|
|
||||||
"imageUrl": "assets/img/1.jpg",
|
|
||||||
"rating": 3.5,
|
|
||||||
"tags": ["test"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"hotelName": "Marrakesch",
|
|
||||||
"description": "Genießen Sie den Blick auf die Berge",
|
|
||||||
"price": 145.5,
|
|
||||||
"imageUrl": "assets/img/2.jpg",
|
|
||||||
"rating": 5,
|
|
||||||
"tags": ["test"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"hotelName": "Abuja neuer Palast",
|
|
||||||
"description": "Kompletter Aufenthalt mit Autoservice",
|
|
||||||
"price": 120.12,
|
|
||||||
"imageUrl": "assets/img/3.jpg",
|
|
||||||
"rating": 4,
|
|
||||||
"tags": ["test"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 4,
|
|
||||||
"hotelName": "Kapstadt Stadt",
|
|
||||||
"description": "Wunderschönes Ambiente für Ihren Aufenthalt",
|
|
||||||
"price": 135.12,
|
|
||||||
"imageUrl": "assets/img/4.jpg",
|
|
||||||
"rating": 2.5,
|
|
||||||
"tags": ["test"]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +1,2 @@
|
|||||||
<input [(ngModel)]="input" (ngModelChange)="update($event)" class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200" type="search">
|
<label for="search">Search</label>
|
||||||
|
<input [(ngModel)]="input" (ngModelChange)="update($event)" id="search" class="border border-gray-300 rounded-md p-2 w-full focus:border-blue-500 focus:ring focus:ring-blue-200" type="search">
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { Component, Input, Output } from "@angular/core";
|
||||||
import { Component, NgModule } from "@angular/core";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { FormsModule, NgForm, NgModel } from "@angular/forms";
|
|
||||||
import { Input, Output } from "@angular/core";
|
|
||||||
import { EventEmitter } from "@angular/core";
|
import { EventEmitter } from "@angular/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||||
|
|
||||||
import { Hotel } from '../HotelItem/hotel';
|
import { Hotel } from '../hotel-item/hotel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initial data for in memory web api
|
* Initial data for in memory web api
|
||||||
|
@ -1,5 +1 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<!-- <p>ID: {{response.id}}</p> -->
|
|
||||||
<!-- <p>userId: {{response.userId}}</p> -->
|
|
||||||
<!-- <p>title: {{response.title}}</p> -->
|
|
||||||
<!-- <p>completed: {{response.completed}}</p> -->
|
|
@ -1,20 +1,12 @@
|
|||||||
import { Component, Injectable } from '@angular/core';
|
import { Component, Injectable } from '@angular/core';
|
||||||
import { HotelItem } from './HotelItem/HotelItem.component';
|
import { HotelService } from './service/hotel.service';
|
||||||
import { SearchComponent } from './Search/search.component';
|
|
||||||
import { AsyncPipe, NgFor, NgForOf, NgIf, UpperCasePipe } from '@angular/common';
|
|
||||||
import { TextPipe } from '../text.pipe';
|
|
||||||
import { HotelService } from './Parent/services/hotel.service';
|
|
||||||
import { inject } from '@angular/core';
|
|
||||||
import { filter, from, last, map, Observable, scan } from 'rxjs';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Hotel } from './HotelItem/hotel';
|
|
||||||
import { RouterOutlet } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
|
|
||||||
@Injectable({providedIn: "root"})
|
@Injectable({providedIn: "root"})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterOutlet, NgFor, NgForOf, NgIf, HotelItem, SearchComponent, UpperCasePipe, TextPipe, AsyncPipe],
|
imports: [RouterOutlet],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
providers: [HotelService],
|
providers: [HotelService],
|
||||||
styleUrl: './app.component.css'
|
styleUrl: './app.component.css'
|
||||||
|
@ -8,6 +8,7 @@ import { registerLocaleData } from '@angular/common';
|
|||||||
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 { HotelData } from './api/api';
|
import { HotelData } from './api/api';
|
||||||
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
|
|
||||||
registerLocaleData(localeDe, 'de-DE');
|
registerLocaleData(localeDe, 'de-DE');
|
||||||
registerLocaleData(localeCn, 'cn-CN');
|
registerLocaleData(localeCn, 'cn-CN');
|
||||||
@ -16,5 +17,5 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(routes),
|
provideRouter(routes),
|
||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData))]
|
importProvidersFrom(InMemoryWebApiModule.forRoot(HotelData)), provideAnimationsAsync()]
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { HotelDetailsComponent } from './hotel-details/hotel-details.component';
|
import { HotelDetailsComponent } from './hotel-details/hotel-details.component';
|
||||||
import { HotelListComponent } from './hotel-list/hotel-list.component';
|
import { HotelListComponent } from './hotel-list/hotel-list.component';
|
||||||
import { TestComponent } from './test/test.component';
|
import { HotelFormComponent } from './hotel-form/hotel-form.component';
|
||||||
import { NewHotelComponent } from './new-hotel/new-hotel.component';
|
import { formGuard } from './form.guard';
|
||||||
|
import { NotFoundComponent } from './not-found/not-found.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@ -12,13 +13,15 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "hotels/:id",
|
path: "hotels/:id",
|
||||||
component: HotelDetailsComponent,
|
component: HotelDetailsComponent,
|
||||||
|
canDeactivate: [formGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "testing",
|
path: "create-hotel",
|
||||||
component: TestComponent,
|
component: HotelFormComponent,
|
||||||
|
canDeactivate: [formGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "new",
|
path: "**",
|
||||||
component: NewHotelComponent,
|
component: NotFoundComponent,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";
|
|
||||||
|
|
||||||
export class CrossValidator {
|
|
||||||
public onlyAllowNameAndDescriptionSame() {
|
|
||||||
return (control: AbstractControl): ValidationErrors | null => {
|
|
||||||
if (!(control instanceof FormGroup)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = control.get('name')?.value;
|
|
||||||
const description = control.get('description')?.value;
|
|
||||||
|
|
||||||
console.log(name, description);
|
|
||||||
|
|
||||||
const error = name !== description ? { mismatch: true } : null;
|
|
||||||
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public crossValidate() {
|
|
||||||
return (control: AbstractControl): ValidationErrors | null => {
|
|
||||||
if (!(control instanceof FormGroup)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactType = control.get('contactType')?.value;
|
|
||||||
|
|
||||||
if (contactType == "None") return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
17
src/app/form.guard.spec.ts
Normal file
17
src/app/form.guard.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CanDeactivateFn } from '@angular/router';
|
||||||
|
|
||||||
|
import { formGuard } from './form.guard';
|
||||||
|
|
||||||
|
describe('formGuard', () => {
|
||||||
|
const executeGuard: CanDeactivateFn = (...guardParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => formGuard(...guardParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeGuard).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
15
src/app/form.guard.ts
Normal file
15
src/app/form.guard.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { CanDeactivateFn } from '@angular/router';
|
||||||
|
import { HotelFormComponent } from './hotel-form/hotel-form.component';
|
||||||
|
import { HotelDetailsComponent } from './hotel-details/hotel-details.component';
|
||||||
|
|
||||||
|
export const formGuard: CanDeactivateFn<HotelFormComponent | HotelDetailsComponent> = (component: HotelFormComponent | HotelDetailsComponent, currentRoute, currentState, nextState) => {
|
||||||
|
const hasUnsavedChanges =
|
||||||
|
(component instanceof HotelFormComponent && component.form?.dirty) ||
|
||||||
|
(component instanceof HotelDetailsComponent && component.hotelForm?.form?.dirty);
|
||||||
|
|
||||||
|
if (hasUnsavedChanges) {
|
||||||
|
return confirm('You have unsaved changes. Do you really want to leave?');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
@if (hotel) {
|
<div class="container p-4 mx-auto max-w-4xl">
|
||||||
<app-hotel-item [hotel]="hotel" [isDetail]="true"></app-hotel-item>
|
<div *ngIf="hotel">
|
||||||
<app-hotel-form [hotel]="hotel"></app-hotel-form>
|
<app-hotel-form [hotel]="hotel" #hotelForm></app-hotel-form>
|
||||||
<button class="delete-button" (click)="delete()">😠 DELETE</button>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Hotel } from '../HotelItem/hotel';
|
import { Hotel } from '../hotel-item/hotel';
|
||||||
import { CurrencyPipe, NgFor, NgForOf, NgIf } from '@angular/common';
|
import { NgIf } from '@angular/common';
|
||||||
import { StarRatingComponent } from '../star-rating/star-rating.component';
|
|
||||||
import { HotelItem } from '../HotelItem/HotelItem.component';
|
|
||||||
import { catchError, EMPTY } from 'rxjs';
|
import { catchError, EMPTY } from 'rxjs';
|
||||||
import { HotelFormComponent } from '../hotel-form/hotel-form.component';
|
import { HotelFormComponent } from "../hotel-form/hotel-form.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hotel-details',
|
selector: 'app-hotel-details',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CurrencyPipe, StarRatingComponent, HotelItem, HotelFormComponent, NgIf, NgForOf],
|
imports: [HotelFormComponent, NgIf],
|
||||||
templateUrl: './hotel-details.component.html',
|
templateUrl: './hotel-details.component.html',
|
||||||
styleUrl: './hotel-details.component.css'
|
styleUrl: './hotel-details.component.css'
|
||||||
})
|
})
|
||||||
export class HotelDetailsComponent implements OnInit {
|
export class HotelDetailsComponent implements OnInit {
|
||||||
public hotel: any;
|
public hotel: any;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { }
|
@ViewChild('hotelForm', { static: false })
|
||||||
|
public hotelForm!: HotelFormComponent;
|
||||||
|
|
||||||
delete(): void {
|
constructor(private route: ActivatedRoute, private http: HttpClient, private router: Router) { }
|
||||||
if (confirm("Are u sure u want to delete this hotel")) {
|
|
||||||
this.http.delete<Hotel>("/api/hotels/" + this.route.snapshot.paramMap.get("id")).subscribe();
|
|
||||||
this.router.navigate(["/"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const routeParams = this.route.snapshot.paramMap;
|
const routeParams = this.route.snapshot.paramMap;
|
||||||
|
@ -1,18 +1,70 @@
|
|||||||
<p>hotel-form works!</p>
|
<form class="space-y-4 max-w-md mx-auto mt-4" [formGroup]="form">
|
||||||
<h1>{{errorMsg}}</h1>
|
<ng-container *ngFor="let field of ['name', 'description', 'price', 'rating']">
|
||||||
<form [formGroup]="hotelForm">
|
<div class="form-group">
|
||||||
<label for="name">Name</label>
|
<label [for]="'hotel' + field" class="block text-sm font-medium text-gray-700">
|
||||||
<div class="text-red-500 text-5xl font-bold" *ngIf="errorMessages['name']">{{ errorMessages['name'] }}</div>
|
Hotel {{field.charAt(0).toUpperCase() + field.slice(1)}}
|
||||||
<input type="text" class="border-red-500 text-3xl border-3 rounded-full bg-gray-500 font-bold p-3 m-3 border-8" id="name" formControlName="name">
|
</label>
|
||||||
<label for="description">Description</label>
|
<input [type]="field === 'price' || field === 'rating' ? 'number' : 'text'" [formControlName]="field"
|
||||||
<div class="text-red" *ngIf="errorMessages['description']">{{ errorMessages['description'] }}</div>
|
class="form-control mt-1 block w-full p-2 border rounded-md shadow-sm"
|
||||||
<input type="text" class="input-field" [class.border-8]='hotelForm.get("description")?.invalid' id="description"
|
[ngClass]="{
|
||||||
formControlName="description">
|
'border-red-500': errorMessage.includes(field),
|
||||||
<label for="price">Price</label>
|
'border-gray-300': !errorMessage.includes(field),
|
||||||
<div class="text-red" *ngIf="errorMessages['price']">{{ errorMessages['price'] }}</div>
|
'focus:border-red-500': errorMessage.includes(field),
|
||||||
<input type="number" class="input-field"
|
'focus:border-blue-500': !errorMessage.includes(field),
|
||||||
[class.border-8]='hotelForm.get("price")?.invalid && hotelForm.get("price")?.touched' id="price"
|
'focus:ring-red-200': errorMessage.includes(field),
|
||||||
formControlName="price">
|
'focus:ring-blue-200': !errorMessage.includes(field)
|
||||||
<button class="submit-button" (click)="submit()" [disabled]="!hotelForm.valid" type="submit">😃 Submit</button>
|
}"
|
||||||
<a class="back-button" routerLink="/">😭 Cancel</a>
|
[id]="'hotel' + field" [placeholder]="'Enter hotel ' + field">
|
||||||
</form>
|
<div *ngIf="errorMessage?.includes(field)" class="text-red-500 text-sm mt-1">
|
||||||
|
{{errorMessage}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hotelTags" class="block text-sm font-medium text-gray-700">Hotel Tags</label>
|
||||||
|
<div formArrayName="tags">
|
||||||
|
<div *ngFor="let tag of tags.controls; let i=index" class="flex items-center space-x-2">
|
||||||
|
<input type="text" [formControlName]="i"
|
||||||
|
class="form-control mt-1 w-full p-2 border rounded-md shadow-sm"
|
||||||
|
[ngClass]="{
|
||||||
|
'border-red-500': errorMessage.includes('tags'),
|
||||||
|
'border-gray-300': !errorMessage.includes('tags'),
|
||||||
|
'focus:border-red-500': errorMessage.includes('tags'),
|
||||||
|
'focus:border-blue-500': !errorMessage.includes('tags'),
|
||||||
|
'focus:ring-red-200': errorMessage.includes('tags'),
|
||||||
|
'focus:ring-blue-200': !errorMessage.includes('tags')
|
||||||
|
}"
|
||||||
|
[id]="'hotelTag' + i" placeholder="Enter hotel tag">
|
||||||
|
<button mat-icon-button (click)="tags.removeAt(i)" class="mt-1">
|
||||||
|
<mat-icon class="text-red-500">delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button type="button" mat-button (click)="addTag()" class="mt-2">Add Tag</button>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="errorMessage.includes('tags')" class="text-red-500 text-sm mt-1">
|
||||||
|
{{errorMessage}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<button type="submit" mat-flat-button (click)="submit()">Submit</button>
|
||||||
|
<button type="button" mat-stroked-button routerLink="/">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<button mat-stroked-button color="warn" aria-label="Delete hotel" *ngIf="hotel" (click)="showDeleteConfirmation = true">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="showDeleteConfirmation" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Confirm Deletion</h2>
|
||||||
|
<p class="mb-6">Are you sure you want to delete this hotel?</p>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<button mat-stroked-button (click)="showDeleteConfirmation = false">Cancel</button>
|
||||||
|
<button mat-flat-button color="warn" (click)="delete()">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -1,77 +1,150 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, inject, Input } from '@angular/core';
|
||||||
import { Hotel } from '../HotelItem/hotel';
|
import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||||
import { AbstractControl, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { Hotel } from "../hotel-item/hotel";
|
||||||
import { NgFor, NgIf } from '@angular/common';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { catchError, debounceTime } from "rxjs";
|
||||||
|
import { Router, RouterLink } from "@angular/router";
|
||||||
|
import { NgClass, NgFor, NgIf } from "@angular/common";
|
||||||
|
import { MatButtonModule } from "@angular/material/button";
|
||||||
|
import { MatIconModule } from "@angular/material/icon";
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hotel-form',
|
selector: 'app-hotel-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterLink,
|
||||||
|
NgIf,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
NgFor,
|
||||||
|
FormsModule,
|
||||||
|
NgClass
|
||||||
|
],
|
||||||
templateUrl: './hotel-form.component.html',
|
templateUrl: './hotel-form.component.html',
|
||||||
styleUrl: './hotel-form.component.css'
|
styleUrl: './hotel-form.component.css'
|
||||||
})
|
})
|
||||||
export class HotelFormComponent {
|
export class HotelFormComponent {
|
||||||
@Input() public hotel!: Hotel;
|
public form!: FormGroup;
|
||||||
public hotelForm!: FormGroup
|
|
||||||
public errorMsg: string = 'testing';
|
|
||||||
errorMessages: Record<string, string> = {};
|
|
||||||
|
|
||||||
constructor(public http: HttpClient, public router: Router) {
|
@Input()
|
||||||
|
public hotel!: Hotel;
|
||||||
|
|
||||||
|
public router: Router = inject(Router);
|
||||||
|
|
||||||
|
public showDeleteConfirmation = false;
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) { }
|
||||||
|
|
||||||
|
public errorMessage: string = '';
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
name: new FormControl(this.hotel?.hotelName || '', [Validators.minLength(3), Validators.required]),
|
||||||
|
description: new FormControl(this.hotel?.description || '', [Validators.minLength(3), Validators.required]),
|
||||||
|
price: new FormControl(this.hotel?.price || '', [Validators.min(0), Validators.required]),
|
||||||
|
rating: new FormControl(this.hotel?.rating || '', [Validators.min(0), Validators.max(5), Validators.required]),
|
||||||
|
tags: new FormArray(this.hotel?.tags?.map(tag => new FormControl(tag)) || [])
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(this.form.controls).forEach(key => {
|
||||||
|
const control = this.form.get(key);
|
||||||
|
control?.valueChanges.pipe(
|
||||||
|
debounceTime(1000)
|
||||||
|
).subscribe(() => {
|
||||||
|
this.setError(control as AbstractControl, key);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private validationErrorMessages: Record<string, string> = {
|
public delete() {
|
||||||
required: "This field is required",
|
this.http.delete(`api/hotels/${this.hotel?.id}`).pipe(
|
||||||
minlength: "The value is too short",
|
catchError(err => {
|
||||||
maxlength: "The value is too long",
|
console.error('Error deleting hotel', err);
|
||||||
pattern: "The value format is incorrect"
|
return err;
|
||||||
};
|
})
|
||||||
|
).subscribe(() => {
|
||||||
|
console.log('Hotel deleted');
|
||||||
|
});
|
||||||
|
|
||||||
setMessage(value: AbstractControl): void {
|
this.router.navigate(['/']);
|
||||||
if ((value.touched || value.dirty) && value.errors) {
|
}
|
||||||
this.errorMsg = Object.keys(value.errors).map(key => {
|
|
||||||
console.log(this.validationErrorMessages[key]);
|
get tags() {
|
||||||
return this.validationErrorMessages[key] || `Unknown error: ${key}`;
|
return this.form.get('tags') as FormArray;
|
||||||
}).join(' ');
|
}
|
||||||
|
|
||||||
|
public addTag() {
|
||||||
|
this.tags.push(new FormControl(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public submit() {
|
||||||
|
if (this.form.valid && !this.errorMessage) {
|
||||||
|
const hotelData: Hotel = {
|
||||||
|
id: this.hotel?.id || 0,
|
||||||
|
hotelName: this.form.value.name,
|
||||||
|
description: this.form.value.description,
|
||||||
|
price: this.form.value.price,
|
||||||
|
rating: this.form.value.rating,
|
||||||
|
imageUrl: this.hotel?.imageUrl || 'https://placehold.co/2000x1050/EEE/31343C',
|
||||||
|
tags: this.form.value.tags || []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.hotel) {
|
||||||
|
this.http.put(`api/hotels/${this.hotel.id}`, hotelData).pipe(
|
||||||
|
catchError(err => {
|
||||||
|
console.error('Error updating hotel', err);
|
||||||
|
return err;
|
||||||
|
})
|
||||||
|
).subscribe(() => {
|
||||||
|
console.log('Hotel updated');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
this.http.post('api/hotels', hotelData).pipe(
|
||||||
|
catchError(err => {
|
||||||
|
console.error('Error creating hotel', err);
|
||||||
|
return err;
|
||||||
|
})
|
||||||
|
).subscribe(() => {
|
||||||
|
console.log('Hotel created');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.form.reset();
|
||||||
|
this.router.navigate(['/']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateErrorMessages(): void {
|
private validationErrors: Record<string, string> = {
|
||||||
this.errorMessages = {};
|
'required': 'This field is required',
|
||||||
|
'minlength': 'This field must be at least 3 characters long',
|
||||||
|
'min': 'This field must be at least 0',
|
||||||
|
'max': 'This field must be at most 5',
|
||||||
|
'name.required': 'Hotel name is required',
|
||||||
|
'name.minlength': 'Hotel name must be at least 3 characters long',
|
||||||
|
'description.required': 'Hotel description is required',
|
||||||
|
'description.minlength': 'Hotel description must be at least 3 characters long',
|
||||||
|
'price.required': 'Hotel price is required',
|
||||||
|
'price.min': 'Hotel price cannot be negative',
|
||||||
|
'rating.required': 'Hotel rating is required',
|
||||||
|
'rating.min': 'Hotel rating must be at least 0',
|
||||||
|
'rating.max': 'Hotel rating cannot be more than 5',
|
||||||
|
'tags.required': 'At least one tag is required',
|
||||||
|
};
|
||||||
|
|
||||||
Object.keys(this.hotelForm.controls).forEach(field => {
|
private setError(control: AbstractControl, controlName: string) {
|
||||||
const control = this.hotelForm.get(field);
|
if ((control.touched || control.dirty) && control.errors) {
|
||||||
|
const errors = Object.keys(control.errors).map(key => {
|
||||||
if (control && control.errors) {
|
const errorKey = `${controlName}.${key}`;
|
||||||
this.errorMessages[field] = Object.keys(control.errors)
|
return this.validationErrors[errorKey] || this.validationErrors[key];
|
||||||
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`)
|
});
|
||||||
.join(' ');
|
this.errorMessage = errors.join(' ');
|
||||||
}
|
} else {
|
||||||
});
|
this.errorMessage = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.hotelForm = new FormGroup({
|
|
||||||
name: new FormControl(this.hotel.hotelName, Validators.required),
|
|
||||||
description: new FormControl(this.hotel.description, Validators.required),
|
|
||||||
price: new FormControl(this.hotel.price, Validators.required),
|
|
||||||
});
|
|
||||||
|
|
||||||
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("name")!));
|
|
||||||
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("description")!));
|
|
||||||
// this.hotelForm.valueChanges.subscribe(data => this.setMessage(this.hotelForm.get("price")!));
|
|
||||||
//
|
|
||||||
this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages());
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(): void {
|
|
||||||
this.hotel.hotelName = this.hotelForm.get("name")?.value;
|
|
||||||
this.hotel.description = this.hotelForm.get("description")?.value;
|
|
||||||
this.hotel.price = this.hotelForm.get("price")?.value;
|
|
||||||
console.log(this.hotelForm.value);
|
|
||||||
this.http.put("/api/hotels/" + this.hotel.id, this.hotel).subscribe();
|
|
||||||
this.router.navigate(["/"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<p class="text-2xl font-bold text-gray-800 mb-2">Name: {{hotel.hotelName}}</p>
|
<p class="text-2xl font-bold text-gray-800 mb-2">Name: {{hotel.hotelName}}</p>
|
||||||
<p class="text-base text-gray-600 mb-4">Description: {{hotel.description}}</p>
|
<p class="text-base text-gray-600 mb-4">Description: {{hotel.description}}</p>
|
||||||
<p class="text-lg font-medium text-green-600 mb-4">Price: {{hotel.price | currency : getCurrencyCode(selectedLanguage) : "symbol" : "2.2-2" : selectedLanguage}}</p>
|
<p class="text-lg font-medium text-green-600 mb-4">Price: {{hotel.price | currency : getCurrencyCode(selectedLanguage) : "symbol" : "2.2-2" : selectedLanguage}}</p>
|
||||||
<select [ngModel]="selectedLanguage" (ngModelChange)="languageChange($event)" class="input-field block w-full p-2 border border-gray-300 rounded-md mb-4 focus:border-blue-500 focus:ring focus:ring-blue-200">
|
<select [ngModel]="selectedLanguage" (ngModelChange)="languageChange($event)" class="block w-full p-2 border border-gray-300 rounded-md mb-4 focus:border-blue-500 focus:ring focus:ring-blue-200">
|
||||||
<option *ngFor="let lang of langs" [value]="lang.code" [selected]="lang.lang == 'de'">{{lang.lang}}</option>
|
<option *ngFor="let lang of langs" [value]="lang.code" [selected]="lang.lang == 'de'">{{lang.lang}}</option>
|
||||||
</select>
|
</select>
|
||||||
<img src="{{hotel.imageUrl}}" alt="Hotel" class="w-full rounded-full h-auto animate-shake shadow-lg mb-4">
|
<img src="{{hotel.imageUrl}}" alt="Hotel" class="w-full h-auto rounded-lg shadow-lg mb-4">
|
||||||
<a *ngIf="!isDetail" routerLink="/hotels/{{hotel.id}}" class="submit-button text-blue-600 hover:text-blue-800 hover:underline mb-4 block">Details</a>
|
<a *ngIf="!isDetail" routerLink="/hotels/{{hotel.id}}" class="text-blue-600 hover:text-blue-800 hover:underline mb-4 block">Details</a>
|
@ -1,17 +1,14 @@
|
|||||||
import { Component, Injectable, Input } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
import { ChildComponent } from "../Child/child.component";
|
|
||||||
import { Hotel } from "./hotel";
|
import { Hotel } from "./hotel";
|
||||||
import { CurrencyPipe, NgFor, NgIf } from "@angular/common";
|
import {CurrencyPipe, NgForOf, NgIf} from "@angular/common";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { StarRatingComponent } from "../star-rating/star-rating.component";
|
|
||||||
import { HttpClient } from "@angular/common/http";
|
|
||||||
import { RouterLink } from "@angular/router";
|
import { RouterLink } from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hotel-item',
|
selector: 'app-hotel-item',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './HotelItem.component.html',
|
templateUrl: './HotelItem.component.html',
|
||||||
imports: [ChildComponent, CurrencyPipe, FormsModule, StarRatingComponent, NgIf, RouterLink, NgFor],
|
imports: [CurrencyPipe, FormsModule, NgIf, RouterLink, NgForOf],
|
||||||
})
|
})
|
||||||
export class HotelItem {
|
export class HotelItem {
|
||||||
|
|
@ -1,10 +1,28 @@
|
|||||||
<div class="container p-4 mx-auto max-w-4xl">
|
<div class="container p-4 mx-auto max-w-4xl">
|
||||||
<h1>{{'hello' | uppercase | text}}</h1>
|
<div class="fixed top-0 left-0 w-full bg-white/80 backdrop-blur-xl z-50">
|
||||||
<a class="submit-button" routerLink="/new">CREATE NEW HOTEL</a>
|
<div class="justify-center max-w-4xl container p-4 mx-auto">
|
||||||
<app-search [(input)]="search"></app-search>
|
<h1 class="">{{'hello' | uppercase | text}}</h1>
|
||||||
@if (hotels[0].hotelName) {
|
<app-search [(input)]="search"></app-search>
|
||||||
|
<button routerLink="/create-hotel" mat-flat-button
|
||||||
|
class="btn btn-primary bg-blue-500 text-white font-semibold py-2 px-4 rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75 my-4">Create
|
||||||
|
New Hotel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="hotels[0].hotelName" class="mt-64">
|
||||||
<div *ngFor="let hotel of hotels">
|
<div *ngFor="let hotel of hotels">
|
||||||
<app-hotel-item *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" [hotel]="hotel"></app-hotel-item>
|
<app-hotel-item *ngIf="hotel.hotelName.toLowerCase().includes(search.toLowerCase())" [hotel]="hotel"></app-hotel-item>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!hotels[0].hotelName" class="mt-64">
|
||||||
|
<div class="animate-pulse space-y-4">
|
||||||
|
<div class="h-8 bg-gray-200 rounded w-52"></div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-11/12"></div>
|
||||||
|
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
|
||||||
|
</div>
|
||||||
|
<div class="h-[300px] bg-gray-200 rounded w-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,23 +1,18 @@
|
|||||||
import { CommonModule, NgFor, NgIf, UpperCasePipe } from '@angular/common';
|
import { NgFor, NgIf, UpperCasePipe } from '@angular/common';
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { TextPipe } from '../../text.pipe';
|
import { TextPipe } from '../../text.pipe';
|
||||||
import { SearchComponent } from '../Search/search.component';
|
import { SearchComponent } from '../search/search.component';
|
||||||
import { HotelService } from '../Parent/services/hotel.service';
|
import { HotelService } from '../service/hotel.service';
|
||||||
import { Hotel } from '../HotelItem/hotel';
|
import { Hotel } from '../hotel-item/hotel';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { filter, from, last, map, Observable, scan } from 'rxjs';
|
import { HotelItem } from '../hotel-item/HotelItem.component';
|
||||||
import { HotelItem } from '../HotelItem/HotelItem.component';
|
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
interface User {
|
|
||||||
name: string;
|
|
||||||
age: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-hotel-list',
|
selector: 'app-hotel-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf, RouterLink],
|
imports: [UpperCasePipe, TextPipe, SearchComponent, HotelItem, NgFor, NgIf, RouterLink, MatButtonModule],
|
||||||
templateUrl: './hotel-list.component.html',
|
templateUrl: './hotel-list.component.html',
|
||||||
styleUrl: './hotel-list.component.css'
|
styleUrl: './hotel-list.component.css'
|
||||||
})
|
})
|
||||||
@ -34,41 +29,5 @@ export class HotelListComponent {
|
|||||||
this.http.get<Array<Hotel>>("api/hotels").subscribe(res => {
|
this.http.get<Array<Hotel>>("api/hotels").subscribe(res => {
|
||||||
this.hotels = res;
|
this.hotels = res;
|
||||||
});
|
});
|
||||||
|
|
||||||
const users = [
|
|
||||||
{ name: "Max", age: 21 },
|
|
||||||
{ name: "Peter", age: 31 },
|
|
||||||
{ name: "Hans", age: 13 },
|
|
||||||
{ name: "Klaus", age: 51 },
|
|
||||||
{ name: "Dieter", age: 1 },
|
|
||||||
{ name: "Jan-Marlon", age: 3 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const stream: Observable<User> = from(users);
|
|
||||||
|
|
||||||
stream.pipe(
|
|
||||||
filter((user) => user.age > 18),
|
|
||||||
scan((acc, user) => acc + user.age, 0),
|
|
||||||
map((ageSum, index) => ageSum / (index + 1)),
|
|
||||||
last(),
|
|
||||||
).subscribe(console.log);
|
|
||||||
|
|
||||||
// const stream: Observable<number | string> = from([5, 1, 2, 12, 5, 14, 17, 5, "testing"]);
|
|
||||||
|
|
||||||
// stream.pipe(
|
|
||||||
// filter((value) => typeof value === "number"),
|
|
||||||
// tap((value) => console.log("Zahl:" + value)),
|
|
||||||
// filter((value: number) => value % 2 === 0),
|
|
||||||
// tap((value) => console.log("Gerade Zahl: " + value)),
|
|
||||||
// toArray(),
|
|
||||||
//).subscribe(console.log);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public test() {
|
|
||||||
console.log(this.search);
|
|
||||||
}
|
|
||||||
|
|
||||||
// public foundHotels = this.hotels.pipe(
|
|
||||||
// filter((hotel) => hotel.hotelName.includes(this.search)),
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
<p>Create Hotel</p>
|
|
||||||
|
|
||||||
<form [formGroup]="hotelForm">
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['form']">{{ errorMessages['form'] }}</div>
|
|
||||||
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['name']">{{ errorMessages['name'] }}</div>
|
|
||||||
<input type="text" class="border-red-500"
|
|
||||||
[class.border-8]='hotelForm.get("name")?.invalid && hotelForm.get("name")?.touched' id="name"
|
|
||||||
formControlName="name">
|
|
||||||
<label for="description">Description</label>
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['description']">{{ errorMessages['description'] }}</div>
|
|
||||||
<input type="text" class="border-red-500"
|
|
||||||
[class.border-8]='hotelForm.get("description")?.invalid && hotelForm.get("description")?.touched' id="description"
|
|
||||||
formControlName="description">
|
|
||||||
<label for="imageUrl">Image</label>
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['imageUrl']">{{ errorMessages['imageUrl'] }}</div>
|
|
||||||
<input type="url" class="border-red-500"
|
|
||||||
[class.border-8]='hotelForm.get("imageUel")?.invalid && hotelForm.get("imageUrl")?.touched' id="imageUrl"
|
|
||||||
formControlName="imageUrl">
|
|
||||||
<label for="price">Price</label>
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['price']">{{ errorMessages['price'] }}</div>
|
|
||||||
<input type="number" class="border-red-500" [class.border-8]='hotelForm.get("price")?.invalid' id="price"
|
|
||||||
formControlName="price">
|
|
||||||
<label for="rating">Rating</label>
|
|
||||||
<div class="text-red-500" *ngIf="errorMessages['rating']">{{ errorMessages['rating'] }}</div>
|
|
||||||
<input type="rating" class="border-red-500" [class.border-8]='hotelForm.get("rating")?.invalid' id="rating"
|
|
||||||
formControlName="rating">
|
|
||||||
<button class="submit-button" (click)="addTag()">Add Tag</button>
|
|
||||||
@for (tag of getTags().controls; track tag) {
|
|
||||||
<input type="tag" class="border-red-500" [class.border-8]='hotelForm.get("tag")?.invalid' id="tag"
|
|
||||||
formControlName="tag">
|
|
||||||
<button (click)="deleteTag(tag)" class="delete-button">delete</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<input type="radio" value='None' formControlName="contactType">None
|
|
||||||
<input type="radio" value='Email' formControlName="contactType">Email
|
|
||||||
<input type="radio" value='SMS' formControlName="contactType">SMS
|
|
||||||
|
|
||||||
@if (hotelForm.get("contactType")?.value == "Email") {
|
|
||||||
<div class="mt-3">
|
|
||||||
<label for="email">Email</label>
|
|
||||||
<input type="email" formControlName="email" id="email">
|
|
||||||
<label for="emailConfirmation">Email Confirmation</label>
|
|
||||||
<input type="email" formControlName="emailConfirmation" id="emailConfirmation">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@if (hotelForm.get("contactType")?.value == "SMS") {
|
|
||||||
<div class="mt-3">
|
|
||||||
<label for="phone">Phone Number</label>
|
|
||||||
<input type="tel" formControlName="phone" id="phone">
|
|
||||||
<label for="phoneConfirmation">Phone Number Confirmation</label>
|
|
||||||
<input type="tel" formControlName="phoneConfirmation" id="phoneConfirmation">
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button class="submit-button" (click)="submit()" [disabled]="!hotelForm.valid" type="submit">Submit</button>
|
|
||||||
<a routerLink="/" class="delete-button">Cancel</a>
|
|
||||||
</form>
|
|
@ -1,23 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
|
|
||||||
import { NewHotelComponent } from './new-hotel.component';
|
|
||||||
|
|
||||||
describe('NewHotelComponent', () => {
|
|
||||||
let component: NewHotelComponent;
|
|
||||||
let fixture: ComponentFixture<NewHotelComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [NewHotelComponent]
|
|
||||||
})
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NewHotelComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,117 +0,0 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
import { AbstractControl, FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
||||||
import { Router, RouterLink } from '@angular/router';
|
|
||||||
import { Hotel } from '../HotelItem/hotel';
|
|
||||||
import { NgFor, NgIf } from '@angular/common';
|
|
||||||
import { CrossValidator } from '../crossValidator.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-new-hotel',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ReactiveFormsModule, NgIf, NgFor, RouterLink],
|
|
||||||
templateUrl: './new-hotel.component.html',
|
|
||||||
styleUrl: './new-hotel.component.css'
|
|
||||||
})
|
|
||||||
export class NewHotelComponent {
|
|
||||||
public hotelForm!: FormGroup
|
|
||||||
public hotel!: Hotel
|
|
||||||
errorMessages: Record<string, string> = {};
|
|
||||||
|
|
||||||
private validationErrorMessages: Record<string, string> = {
|
|
||||||
required: "This field is required",
|
|
||||||
minlength: "The value is too short",
|
|
||||||
maxlength: "The value is too long",
|
|
||||||
pattern: "The value format is incorrect"
|
|
||||||
};
|
|
||||||
|
|
||||||
private fullFormValidationErrorMessages: Record<string, string> = {
|
|
||||||
mismatch: "The name and description have to be the same",
|
|
||||||
}
|
|
||||||
|
|
||||||
updateErrorMessages(): void {
|
|
||||||
this.errorMessages = {};
|
|
||||||
|
|
||||||
Object.keys(this.hotelForm.controls).forEach(field => {
|
|
||||||
const control = this.hotelForm.get(field);
|
|
||||||
|
|
||||||
if (control && control.errors && control.touched) {
|
|
||||||
this.errorMessages[field] = Object.keys(control.errors)
|
|
||||||
.map(errorKey => this.validationErrorMessages[errorKey] || `Unknown error: ${errorKey}`)
|
|
||||||
.join(' ');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (this.hotelForm.errors) {
|
|
||||||
Object.keys(this.hotelForm.errors).forEach(errorKey => {
|
|
||||||
const errorMessage = this.fullFormValidationErrorMessages[errorKey] || `Unknown error: ${errorKey}`;
|
|
||||||
this.errorMessages['form'] = errorMessage;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(this.errorMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
const tags = [];
|
|
||||||
|
|
||||||
for (const tag of this.hotel?.tags ?? []) {
|
|
||||||
tags.push(new FormControl(tag, [Validators.required]));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.hotelForm = new FormGroup({
|
|
||||||
name: new FormControl("", Validators.required),
|
|
||||||
description: new FormControl("", Validators.required),
|
|
||||||
price: new FormControl(0, Validators.required),
|
|
||||||
imageUrl: new FormControl("", Validators.required),
|
|
||||||
rating: new FormControl(0, Validators.required),
|
|
||||||
contactType: new FormControl('None'),
|
|
||||||
email: new FormControl(''),
|
|
||||||
emailConfirmation: new FormControl(''),
|
|
||||||
phone: new FormControl(''),
|
|
||||||
phoneConfirmation: new FormControl(''),
|
|
||||||
tags: new FormArray(tags)
|
|
||||||
}, { validators: new CrossValidator().onlyAllowNameAndDescriptionSame() })
|
|
||||||
|
|
||||||
this.hotelForm.valueChanges.subscribe(() => this.updateErrorMessages());
|
|
||||||
this.hotelForm.valueChanges.subscribe(() => this.formUpdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
formUpdated() {
|
|
||||||
console.log(this.hotelForm.get("contactType")?.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTags() {
|
|
||||||
return this.hotelForm.controls['tags'] as FormArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTag(tagElement: AbstractControl) {
|
|
||||||
this.getTags().removeAt(this.getTags().controls.indexOf(tagElement));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
addTag() {
|
|
||||||
this.getTags().push(new FormControl('', [Validators.required]));
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(public http: HttpClient, public router: Router) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(): void {
|
|
||||||
console.log(this.hotelForm.value);
|
|
||||||
const hotel: Hotel = {
|
|
||||||
id: 0,
|
|
||||||
hotelName: this.hotelForm.get("name")?.value,
|
|
||||||
description: this.hotelForm.get("description")?.value,
|
|
||||||
price: this.hotelForm.get("price")?.value,
|
|
||||||
imageUrl: this.hotelForm.get("imageUrl")?.value,
|
|
||||||
rating: this.hotelForm.get("rating")?.value,
|
|
||||||
tags: this.hotelForm.get("tags")?.value
|
|
||||||
}
|
|
||||||
this.http.post("/api/hotels/", hotel).subscribe();
|
|
||||||
this.router.navigate(["/"]);
|
|
||||||
}
|
|
||||||
}
|
|
7
src/app/not-found/not-found.component.html
Normal file
7
src/app/not-found/not-found.component.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-9xl font-bold text-gray-800">404</h1>
|
||||||
|
<p class="text-2xl font-medium text-gray-600 mb-8">Page Not Found</p>
|
||||||
|
<a type="submit" mat-flat-button routerLink="/">Return Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,18 +1,18 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { TestComponent } from './test.component';
|
import { NotFoundComponent } from './not-found.component';
|
||||||
|
|
||||||
describe('TestComponent', () => {
|
describe('NotFoundComponent', () => {
|
||||||
let component: TestComponent;
|
let component: NotFoundComponent;
|
||||||
let fixture: ComponentFixture<TestComponent>;
|
let fixture: ComponentFixture<NotFoundComponent>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [TestComponent]
|
imports: [NotFoundComponent]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
|
||||||
fixture = TestBed.createComponent(TestComponent);
|
fixture = TestBed.createComponent(NotFoundComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
17
src/app/not-found/not-found.component.ts
Normal file
17
src/app/not-found/not-found.component.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-not-found',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatButtonModule,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './not-found.component.html',
|
||||||
|
styleUrl: './not-found.component.css'
|
||||||
|
})
|
||||||
|
export class NotFoundComponent {
|
||||||
|
|
||||||
|
}
|
51
src/app/service/hotel.service.ts
Normal file
51
src/app/service/hotel.service.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Hotel } from "../hotel-item/hotel";
|
||||||
|
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
||||||
|
import { Observable, throwError, catchError } from "rxjs";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HotelService {
|
||||||
|
private apiUrl = 'api/hotels';
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
private handleError(error: HttpErrorResponse) {
|
||||||
|
let errorMessage = 'An error occurred';
|
||||||
|
if (error.error instanceof ErrorEvent) {
|
||||||
|
// Client-side error
|
||||||
|
errorMessage = `Error: ${error.error.message}`;
|
||||||
|
} else {
|
||||||
|
// Server-side error
|
||||||
|
errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
|
||||||
|
}
|
||||||
|
console.error(errorMessage);
|
||||||
|
return throwError(() => errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHotels(): Observable<Hotel[]> {
|
||||||
|
return this.http.get<Hotel[]>(this.apiUrl)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHotel(id: number): Observable<Hotel> {
|
||||||
|
return this.http.get<Hotel>(`${this.apiUrl}/${id}`)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
|
||||||
|
public createHotel(hotel: Hotel): Observable<Hotel> {
|
||||||
|
return this.http.post<Hotel>(this.apiUrl, hotel)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateHotel(hotel: Hotel): Observable<Hotel> {
|
||||||
|
return this.http.put<Hotel>(`${this.apiUrl}/${hotel.id}`, hotel)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteHotel(id: number): Observable<{}> {
|
||||||
|
return this.http.delete(`${this.apiUrl}/${id}`)
|
||||||
|
.pipe(catchError(this.handleError));
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
<p>test works!</p>
|
|
||||||
|
|
||||||
<form [formGroup]="loginForm">
|
|
||||||
<label for="username">Username</label>
|
|
||||||
<input type="text" id="username" autocomplete="username" formControlName="username">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input type="password" id="password" formControlName="password">
|
|
||||||
<button (click)="submit()" type="submit">Login</button>
|
|
||||||
<button type="reset" (click)="reset()">Reset</button>
|
|
||||||
</form>
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-test',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ReactiveFormsModule],
|
|
||||||
templateUrl: './test.component.html',
|
|
||||||
styleUrl: './test.component.css'
|
|
||||||
})
|
|
||||||
export class TestComponent {
|
|
||||||
public loginForm!: FormGroup;
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
|
||||||
this.loginForm = this.setUpLoginForm();
|
|
||||||
|
|
||||||
console.log(this.loginForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
setUpLoginForm(): FormGroup {
|
|
||||||
return new FormGroup({
|
|
||||||
username: new FormControl("Jan"),
|
|
||||||
password: new FormControl('')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.loginForm = this.setUpLoginForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(): void {
|
|
||||||
console.log(this.loginForm.value);
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,8 +7,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<link rel="stylesheet" href="./styles.css">
|
<link rel="stylesheet" href="./styles.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-pink-600 p-32">
|
<body class="mat-typography">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -7,18 +7,5 @@
|
|||||||
@apply border-black rounded px-3 py-1 border-[3px];
|
@apply border-black rounded px-3 py-1 border-[3px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field {
|
html, body { height: 100%; }
|
||||||
@apply border-red-500 text-3xl rounded-full bg-gray-500 font-bold p-3 m-3 border-8
|
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
|
||||||
}
|
|
||||||
|
|
||||||
.back-button {
|
|
||||||
@apply border-black rounded bg-blue-500 border-[30px] m-3 text-3xl font-bold p-3
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-button {
|
|
||||||
@apply border-black rounded-full bg-green-500 border-[30px] m-3 text-3xl font-bold p-3
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
@apply border-black rounded bg-red-500 border-[30px] m-3 text-3xl font-bold p-3
|
|
||||||
}
|
|
@ -1,28 +1,9 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
|
||||||
const colors = require('tailwindcss/colors');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./src/**/*.{html,ts}",
|
"./src/**/*.{html,ts}",
|
||||||
],
|
],
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
...colors,
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
shake: {
|
|
||||||
'0%, 100%': { transform: 'translateX(0)' },
|
|
||||||
'25%': { transform: 'translateX(-30px)' },
|
|
||||||
'75%': { transform: 'translateX(30px)' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
animation: {
|
|
||||||
shake: 'shake 10s ease-in-out infinite',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user